2281 lines
		
	
	
	
		
			66 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			2281 lines
		
	
	
	
		
			66 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // This file is part of Desktop App Toolkit,
 | |
| // a set of libraries for developing nice desktop applications.
 | |
| //
 | |
| // For license and copyright information please follow this link:
 | |
| // https://github.com/desktop-app/legal/blob/master/LEGAL
 | |
| //
 | |
| #include "ui/text/text_renderer.h"
 | |
| 
 | |
| #include "ui/text/text_extended_data.h"
 | |
| #include "styles/style_basic.h"
 | |
| 
 | |
| #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
 | |
| #include <private/qharfbuzz_p.h>
 | |
| #endif // Qt < 6.0.0
 | |
| 
 | |
| namespace Ui::Text {
 | |
| namespace {
 | |
| 
 | |
| // COPIED FROM qtextengine.cpp AND MODIFIED
 | |
| 
 | |
| struct BidiStatus {
 | |
| 	BidiStatus() {
 | |
| 		eor = QChar::DirON;
 | |
| 		lastStrong = QChar::DirON;
 | |
| 		last = QChar::DirON;
 | |
| 		dir = QChar::DirON;
 | |
| 	}
 | |
| 	QChar::Direction eor;
 | |
| 	QChar::Direction lastStrong;
 | |
| 	QChar::Direction last;
 | |
| 	QChar::Direction dir;
 | |
| };
 | |
| 
 | |
| enum { _MaxBidiLevel = 61 };
 | |
| enum { _MaxItemLength = 4096 };
 | |
| 
 | |
| void InitTextItemWithScriptItem(QTextItemInt &ti, const QScriptItem &si) {
 | |
| 	// explicitly initialize flags so that initFontAttributes can be called
 | |
| 	// multiple times on the same TextItem
 | |
| 	ti.flags = { };
 | |
| 	if (si.analysis.bidiLevel % 2)
 | |
| 		ti.flags |= QTextItem::RightToLeft;
 | |
| 	ti.ascent = si.ascent;
 | |
| 	ti.descent = si.descent;
 | |
| 
 | |
| 	if (ti.charFormat.hasProperty(QTextFormat::TextUnderlineStyle)) {
 | |
| 		ti.underlineStyle = ti.charFormat.underlineStyle();
 | |
| 	} else if (ti.charFormat.boolProperty(QTextFormat::FontUnderline)
 | |
| 		|| ti.f->underline()) {
 | |
| 		ti.underlineStyle = QTextCharFormat::SingleUnderline;
 | |
| 	}
 | |
| 
 | |
| 	// compat
 | |
| 	if (ti.underlineStyle == QTextCharFormat::SingleUnderline)
 | |
| 		ti.flags |= QTextItem::Underline;
 | |
| 
 | |
| 	if (ti.f->overline() || ti.charFormat.fontOverline())
 | |
| 		ti.flags |= QTextItem::Overline;
 | |
| 	if (ti.f->strikeOut() || ti.charFormat.fontStrikeOut())
 | |
| 		ti.flags |= QTextItem::StrikeOut;
 | |
| }
 | |
| 
 | |
| void AppendRange(
 | |
| 		QVarLengthArray<FixedRange> &ranges,
 | |
| 		FixedRange range) {
 | |
| 	for (auto i = ranges.begin(); i != ranges.end(); ++i) {
 | |
| 		if (range.till < i->from) {
 | |
| 			ranges.insert(i, range);
 | |
| 			return;
 | |
| 		} else if (!Distinct(range, *i)) {
 | |
| 			*i = United(*i, range);
 | |
| 			for (auto j = i + 1; j != ranges.end(); ++j) {
 | |
| 				if (j->from > i->till) {
 | |
| 					ranges.erase(i + 1, j);
 | |
| 					return;
 | |
| 				} else {
 | |
| 					*i = United(*i, *j);
 | |
| 				}
 | |
| 			}
 | |
| 			ranges.erase(i + 1, ranges.end());
 | |
| 			return;
 | |
| 		}
 | |
| 	}
 | |
| 	ranges.push_back(range);
 | |
| }
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| struct Renderer::BidiControl {
 | |
| 	inline BidiControl(bool rtl)
 | |
| 		: base(rtl ? 1 : 0), level(rtl ? 1 : 0) {}
 | |
| 
 | |
| 	inline void embed(bool rtl, bool o = false) {
 | |
| 		unsigned int toAdd = 1;
 | |
| 		if ((level % 2 != 0) == rtl) {
 | |
| 			++toAdd;
 | |
| 		}
 | |
| 		if (level + toAdd <= _MaxBidiLevel) {
 | |
| 			ctx[cCtx].level = level;
 | |
| 			ctx[cCtx].override = override;
 | |
| 			cCtx++;
 | |
| 			override = o;
 | |
| 			level += toAdd;
 | |
| 		}
 | |
| 	}
 | |
| 	inline bool canPop() const { return cCtx != 0; }
 | |
| 	inline void pdf() {
 | |
| 		Q_ASSERT(cCtx);
 | |
| 		--cCtx;
 | |
| 		level = ctx[cCtx].level;
 | |
| 		override = ctx[cCtx].override;
 | |
| 	}
 | |
| 
 | |
| 	inline QChar::Direction basicDirection() const {
 | |
| 		return (base ? QChar::DirR : QChar::DirL);
 | |
| 	}
 | |
| 	inline unsigned int baseLevel() const {
 | |
| 		return base;
 | |
| 	}
 | |
| 	inline QChar::Direction direction() const {
 | |
| 		return ((level % 2) ? QChar::DirR : QChar::DirL);
 | |
| 	}
 | |
| 
 | |
| 	struct {
 | |
| 		unsigned int level = 0;
 | |
| 		bool override = false;
 | |
| 	} ctx[_MaxBidiLevel];
 | |
| 	unsigned int cCtx = 0;
 | |
| 	const unsigned int base;
 | |
| 	unsigned int level;
 | |
| 	bool override = false;
 | |
| };
 | |
| 
 | |
| FixedRange Intersected(FixedRange a, FixedRange b) {
 | |
| 	return {
 | |
| 		.from = std::max(a.from, b.from),
 | |
| 		.till = std::min(a.till, b.till),
 | |
| 	};
 | |
| }
 | |
| 
 | |
| bool Intersects(FixedRange a, FixedRange b) {
 | |
| 	return (a.till > b.from) && (b.till > a.from);
 | |
| }
 | |
| 
 | |
| FixedRange United(FixedRange a, FixedRange b) {
 | |
| 	return {
 | |
| 		.from = std::min(a.from, b.from),
 | |
| 		.till = std::max(a.till, b.till),
 | |
| 	};
 | |
| }
 | |
| 
 | |
| bool Distinct(FixedRange a, FixedRange b) {
 | |
| 	return (a.till < b.from) || (b.till < a.from);
 | |
| }
 | |
| 
 | |
| Renderer::Renderer(const Ui::Text::String &t)
 | |
| : _t(&t)
 | |
| , _spoiler(_t->_extended ? _t->_extended->spoiler.get() : nullptr) {
 | |
| }
 | |
| 
 | |
| Renderer::~Renderer() {
 | |
| 	restoreAfterElided();
 | |
| 	if (_p) {
 | |
| 		_p->setPen(_originalPen);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Renderer::draw(QPainter &p, const PaintContext &context) {
 | |
| 	if (_t->isEmpty()) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	_p = &p;
 | |
| 	_p->setFont(_t->_st->font);
 | |
| 	_palette = context.palette ? context.palette : &st::defaultTextPalette;
 | |
| 	_colors = context.colors;
 | |
| 	_originalPen = _p->pen();
 | |
| 	_originalPenSelected = (_palette->selectFg->c.alphaF() == 0)
 | |
| 		? _originalPen
 | |
| 		: _palette->selectFg->p;
 | |
| 
 | |
| 	_x = _startLeft = context.position.x();
 | |
| 	_y = _startTop = context.position.y();
 | |
| 	_yFrom = context.clip.isNull() ? 0 : context.clip.y();
 | |
| 	_yTo = context.clip.isNull()
 | |
| 		? -1
 | |
| 		: (context.clip.y() + context.clip.height());
 | |
| 	_geometry = context.geometry.layout
 | |
| 		? context.geometry
 | |
| 		: SimpleGeometry(
 | |
| 			context.availableWidth,
 | |
| 			(context.elisionLines
 | |
| 				? context.elisionLines
 | |
| 				: (context.elisionHeight / _t->_st->font->height)),
 | |
| 			context.elisionRemoveFromEnd,
 | |
| 			context.elisionBreakEverywhere);
 | |
| 	_breakEverywhere = _geometry.breakEverywhere;
 | |
| 	_spoilerCache = context.spoiler;
 | |
| 	_selection = context.selection;
 | |
| 	_highlight = context.highlight;
 | |
| 	_fullWidthSelection = context.fullWidthSelection;
 | |
| 	_align = context.align;
 | |
| 	_cachedNow = context.now;
 | |
| 	_pausedEmoji = context.paused || context.pausedEmoji;
 | |
| 	_pausedSpoiler = context.paused || context.pausedSpoiler;
 | |
| 	_spoilerOpacity = _spoiler
 | |
| 		? (1. - _spoiler->revealAnimation.value(
 | |
| 			_spoiler->revealed ? 1. : 0.))
 | |
| 		: 0.;
 | |
| 	_quotePreCache = context.pre;
 | |
| 	_quoteBlockquoteCache = context.blockquote;
 | |
| 	enumerate();
 | |
| }
 | |
| 
 | |
| void Renderer::enumerate() {
 | |
| 	Expects(!_geometry.outElided);
 | |
| 
 | |
| 	_blocksSize = _t->_blocks.size();
 | |
| 	_str = _t->_text.unicode();
 | |
| 
 | |
| 	if (_p) {
 | |
| 		const auto clip = _p->hasClipping() ? _p->clipBoundingRect() : QRect();
 | |
| 		if (clip.width() > 0 || clip.height() > 0) {
 | |
| 			if (_yFrom < clip.y()) {
 | |
| 				_yFrom = clip.y();
 | |
| 			}
 | |
| 			if (_yTo < 0 || _yTo > clip.y() + clip.height()) {
 | |
| 				_yTo = clip.y() + clip.height();
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if ((*_t->_blocks.cbegin())->type() != TextBlockType::Newline) {
 | |
| 		initNextParagraph(
 | |
| 			_t->_blocks.cbegin(),
 | |
| 			_t->_startQuoteIndex,
 | |
| 			UnpackParagraphDirection(
 | |
| 				_t->_startParagraphLTR,
 | |
| 				_t->_startParagraphRTL));
 | |
| 	}
 | |
| 
 | |
| 	_lineHeight = 0;
 | |
| 	_fontHeight = _t->_st->font->height;
 | |
| 	auto last_rBearing = QFixed(0);
 | |
| 	_last_rPadding = QFixed(0);
 | |
| 
 | |
| 	const auto guard = gsl::finally([&] {
 | |
| 		if (_p) {
 | |
| 			paintSpoilerRects();
 | |
| 		}
 | |
| 		if (_highlight) {
 | |
| 			composeHighlightPath();
 | |
| 		}
 | |
| 	});
 | |
| 
 | |
| 	auto blockIndex = 0;
 | |
| 	bool longWordLine = true;
 | |
| 	auto e = _t->_blocks.cend();
 | |
| 	for (auto i = _t->_blocks.cbegin(); i != e; ++i, ++blockIndex) {
 | |
| 		auto b = i->get();
 | |
| 		auto _btype = b->type();
 | |
| 		auto blockHeight = CountBlockHeight(b, _t->_st);
 | |
| 
 | |
| 		if (_btype == TextBlockType::Newline) {
 | |
| 			if (!_lineHeight) {
 | |
| 				_lineHeight = blockHeight;
 | |
| 			}
 | |
| 			const auto qindex = static_cast<const NewlineBlock*>(b)->quoteIndex();
 | |
| 			const auto changed = (_quoteIndex != qindex);
 | |
| 			fillParagraphBg(changed ? _quotePadding.bottom() : 0);
 | |
| 			if (!drawLine((*i)->position(), i, e)) {
 | |
| 				return;
 | |
| 			}
 | |
| 
 | |
| 			_y += _lineHeight;
 | |
| 			_lineHeight = 0;
 | |
| 
 | |
| 			last_rBearing = 0;
 | |
| 			_last_rPadding = 0;
 | |
| 
 | |
| 			initNextParagraph(
 | |
| 				i + 1,
 | |
| 				qindex,
 | |
| 				static_cast<const NewlineBlock*>(b)->paragraphDirection());
 | |
| 
 | |
| 			longWordLine = true;
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		auto b__f_rbearing = b->f_rbearing();
 | |
| 		auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + b->f_width() - b__f_rbearing);
 | |
| 		if (newWidthLeft >= 0) {
 | |
| 			last_rBearing = b__f_rbearing;
 | |
| 			_last_rPadding = b->f_rpadding();
 | |
| 			_wLeft = newWidthLeft;
 | |
| 
 | |
| 			_lineHeight = qMax(_lineHeight, blockHeight);
 | |
| 
 | |
| 			longWordLine = false;
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		if (_btype == TextBlockType::Text) {
 | |
| 			auto t = static_cast<const TextBlock*>(b);
 | |
| 			if (t->_words.isEmpty()) { // no words in this block, spaces only => layout this block in the same line
 | |
| 				_last_rPadding += b->f_rpadding();
 | |
| 
 | |
| 				_lineHeight = qMax(_lineHeight, blockHeight);
 | |
| 
 | |
| 				longWordLine = false;
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			auto f_wLeft = _wLeft; // vars for saving state of the last word start
 | |
| 			auto f_lineHeight = _lineHeight; // f points to the last word-start element of t->_words
 | |
| 			for (auto j = t->_words.cbegin(), en = t->_words.cend(), f = j; j != en; ++j) {
 | |
| 				auto wordEndsHere = (j->f_width() >= 0);
 | |
| 				auto j_width = wordEndsHere ? j->f_width() : -j->f_width();
 | |
| 
 | |
| 				auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + j_width - j->f_rbearing());
 | |
| 				if (newWidthLeft >= 0) {
 | |
| 					last_rBearing = j->f_rbearing();
 | |
| 					_last_rPadding = j->f_rpadding();
 | |
| 					_wLeft = newWidthLeft;
 | |
| 
 | |
| 					_lineHeight = qMax(_lineHeight, blockHeight);
 | |
| 
 | |
| 					if (wordEndsHere) {
 | |
| 						longWordLine = false;
 | |
| 					}
 | |
| 					if (wordEndsHere || longWordLine) {
 | |
| 						f = j + 1;
 | |
| 						f_wLeft = _wLeft;
 | |
| 						f_lineHeight = _lineHeight;
 | |
| 					}
 | |
| 					continue;
 | |
| 				}
 | |
| 
 | |
| 				if (_elidedLine) {
 | |
| 					_lineHeight = qMax(_lineHeight, blockHeight);
 | |
| 				} else if (f != j && !_breakEverywhere) {
 | |
| 					// word did not fit completely, so we roll back the state to the beginning of this long word
 | |
| 					j = f;
 | |
| 					_wLeft = f_wLeft;
 | |
| 					_lineHeight = f_lineHeight;
 | |
| 					j_width = (j->f_width() >= 0) ? j->f_width() : -j->f_width();
 | |
| 				}
 | |
| 				const auto lineEnd = !_elidedLine
 | |
| 					? j->position()
 | |
| 					: (j + 1 != en)
 | |
| 					? (j + 1)->position()
 | |
| 					: _t->countBlockEnd(i, e);
 | |
| 				fillParagraphBg(0);
 | |
| 				if (!drawLine(lineEnd, i, e)) {
 | |
| 					return;
 | |
| 				}
 | |
| 				_y += _lineHeight;
 | |
| 				_lineHeight = qMax(0, blockHeight);
 | |
| 				_lineStart = j->position();
 | |
| 				_lineStartBlock = blockIndex;
 | |
| 				initNextLine();
 | |
| 
 | |
| 				last_rBearing = j->f_rbearing();
 | |
| 				_last_rPadding = j->f_rpadding();
 | |
| 				_wLeft -= j_width - last_rBearing;
 | |
| 
 | |
| 				longWordLine = !wordEndsHere;
 | |
| 				f = j + 1;
 | |
| 				f_wLeft = _wLeft;
 | |
| 				f_lineHeight = _lineHeight;
 | |
| 			}
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		if (_elidedLine) {
 | |
| 			_lineHeight = qMax(_lineHeight, blockHeight);
 | |
| 		}
 | |
| 		const auto lineEnd = !_elidedLine
 | |
| 			? b->position()
 | |
| 			: _t->countBlockEnd(i, e);
 | |
| 		fillParagraphBg(0);
 | |
| 		if (!drawLine(lineEnd, i, e)) {
 | |
| 			return;
 | |
| 		}
 | |
| 		_y += _lineHeight;
 | |
| 		_lineHeight = qMax(0, blockHeight);
 | |
| 		_lineStart = b->position();
 | |
| 		_lineStartBlock = blockIndex;
 | |
| 		initNextLine();
 | |
| 
 | |
| 		last_rBearing = b__f_rbearing;
 | |
| 		_last_rPadding = b->f_rpadding();
 | |
| 		_wLeft -= b->f_width() - last_rBearing;
 | |
| 
 | |
| 		longWordLine = true;
 | |
| 		continue;
 | |
| 	}
 | |
| 	if (_lineStart < _t->_text.size()) {
 | |
| 		fillParagraphBg(_quotePadding.bottom());
 | |
| 		if (!drawLine(_t->_text.size(), e, e)) {
 | |
| 			return;
 | |
| 		}
 | |
| 	}
 | |
| 	if (!_p && _lookupSymbol) {
 | |
| 		_lookupResult.symbol = _t->_text.size();
 | |
| 		_lookupResult.afterSymbol = false;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Renderer::fillParagraphBg(int paddingBottom) {
 | |
| 	if (_quote) {
 | |
| 		const auto &st = _t->quoteStyle(_quote);
 | |
| 		const auto skip = st.verticalSkip;
 | |
| 		const auto isTop = (_y != _quoteLineTop);
 | |
| 		const auto isBottom = (paddingBottom != 0);
 | |
| 		const auto left = _startLeft + _quoteShift;
 | |
| 		const auto start = _quoteTop + skip;
 | |
| 		const auto top = _quoteLineTop + (isTop ? skip : 0);
 | |
| 		const auto fill = _y + _lineHeight + paddingBottom - top
 | |
| 			- (isBottom ? skip : 0);
 | |
| 		const auto rect = QRect(left, top, _startLineWidth, fill);
 | |
| 
 | |
| 		const auto cache = (!_p || !_quote)
 | |
| 			? nullptr
 | |
| 			: _quote->pre
 | |
| 			? _quotePreCache
 | |
| 			: _quote->blockquote
 | |
| 			? _quoteBlockquoteCache
 | |
| 			: nullptr;
 | |
| 		if (cache) {
 | |
| 			auto &valid = _quote->pre
 | |
| 				? _quotePreValid
 | |
| 				: _quoteBlockquoteValid;
 | |
| 			if (!valid) {
 | |
| 				valid = true;
 | |
| 				ValidateQuotePaintCache(*cache, st);
 | |
| 			}
 | |
| 			FillQuotePaint(*_p, rect, *cache, st, {
 | |
| 				.skippedTop = uint32(top - start),
 | |
| 				.skipBottom = !isBottom,
 | |
| 			});
 | |
| 		}
 | |
| 		if (isTop && st.header > 0) {
 | |
| 			if (_p) {
 | |
| 				const auto font = _t->_st->font->monospace();
 | |
| 				const auto topleft = rect.topLeft();
 | |
| 				const auto position = topleft + st.headerPosition;
 | |
| 				const auto lbaseline = position + QPoint(0, font->ascent);
 | |
| 				_p->setFont(font);
 | |
| 				_p->setPen(_palette->monoFg->p);
 | |
| 				_p->drawText(lbaseline, _t->quoteHeaderText(_quote));
 | |
| 			} else if (_lookupX >= left
 | |
| 				&& _lookupX < left + _startLineWidth
 | |
| 				&& _lookupY >= top
 | |
| 				&& _lookupY < top + st.header) {
 | |
| 				if (_lookupLink) {
 | |
| 					_lookupResult.link = _quote->copy;
 | |
| 				}
 | |
| 				if (_lookupSymbol) {
 | |
| 					_lookupResult.symbol = _lineStart;
 | |
| 					_lookupResult.afterSymbol = false;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	_quoteLineTop = _y + _lineHeight + paddingBottom;
 | |
| }
 | |
| 
 | |
| StateResult Renderer::getState(
 | |
| 		QPoint point,
 | |
| 		GeometryDescriptor geometry,
 | |
| 		StateRequest request) {
 | |
| 	if (_t->isEmpty() || point.y() < 0) {
 | |
| 		return {};
 | |
| 	}
 | |
| 	_lookupRequest = request;
 | |
| 	_lookupX = point.x();
 | |
| 	_lookupY = point.y();
 | |
| 
 | |
| 	_lookupSymbol = (_lookupRequest.flags & StateRequest::Flag::LookupSymbol);
 | |
| 	_lookupLink = (_lookupRequest.flags & StateRequest::Flag::LookupLink);
 | |
| 	if (!_lookupSymbol && _lookupX < 0) {
 | |
| 		return {};
 | |
| 	}
 | |
| 	_geometry = std::move(geometry);
 | |
| 	_breakEverywhere = _geometry.breakEverywhere;
 | |
| 	_yFrom = _lookupY;
 | |
| 	_yTo = _lookupY + 1;
 | |
| 	_align = _lookupRequest.align;
 | |
| 	enumerate();
 | |
| 	return _lookupResult;
 | |
| }
 | |
| 
 | |
| crl::time Renderer::now() const {
 | |
| 	if (!_cachedNow) {
 | |
| 		_cachedNow = crl::now();
 | |
| 	}
 | |
| 	return _cachedNow;
 | |
| }
 | |
| 
 | |
| void Renderer::initNextParagraph(
 | |
| 		String::TextBlocks::const_iterator i,
 | |
| 		int16 paragraphIndex,
 | |
| 		Qt::LayoutDirection direction) {
 | |
| 	_paragraphDirection = (direction == Qt::LayoutDirectionAuto)
 | |
| 		? style::LayoutDirection()
 | |
| 		: direction;
 | |
| 	_paragraphStartBlock = i;
 | |
| 	if (_quoteIndex != paragraphIndex) {
 | |
| 		_y += _quotePadding.bottom();
 | |
| 		_quoteIndex = paragraphIndex;
 | |
| 		_quote = _t->quoteByIndex(paragraphIndex);
 | |
| 		_quotePadding = _t->quotePadding(_quote);
 | |
| 		_quoteTop = _quoteLineTop = _y;
 | |
| 		_y += _quotePadding.top();
 | |
| 		_quotePadding.setTop(0);
 | |
| 		_quoteDirection = _paragraphDirection;
 | |
| 	}
 | |
| 	const auto e = _t->_blocks.cend();
 | |
| 	if (i == e) {
 | |
| 		_lineStart = _paragraphStart = _t->_text.size();
 | |
| 		_lineStartBlock = _t->_blocks.size();
 | |
| 		_paragraphLength = 0;
 | |
| 	} else {
 | |
| 		_lineStart = _paragraphStart = (*i)->position();
 | |
| 		_lineStartBlock = i - _t->_blocks.cbegin();
 | |
| 		for (; i != e; ++i) {
 | |
| 			if ((*i)->type() == TextBlockType::Newline) {
 | |
| 				break;
 | |
| 			}
 | |
| 		}
 | |
| 		_paragraphLength = ((i == e)
 | |
| 			? _t->_text.size()
 | |
| 			: (*i)->position())
 | |
| 			- _paragraphStart;
 | |
| 	}
 | |
| 	_paragraphAnalysis.resize(0);
 | |
| 	initNextLine();
 | |
| }
 | |
| 
 | |
| void Renderer::initNextLine() {
 | |
| 	const auto line = _geometry.layout(_lineIndex++);
 | |
| 	_x = _startLeft + line.left + _quotePadding.left();
 | |
| 	_startLineWidth = line.width;
 | |
| 	_quoteShift = 0;
 | |
| 	if (_quote && _quote->maxWidth < _startLineWidth) {
 | |
| 		const auto delta = _startLineWidth - _quote->maxWidth;
 | |
| 		_startLineWidth = _quote->maxWidth;
 | |
| 
 | |
| 		if (_align & Qt::AlignHCenter) {
 | |
| 			_quoteShift = delta / 2;
 | |
| 		} else if (((_align & Qt::AlignLeft)
 | |
| 			&& (_quoteDirection == Qt::RightToLeft))
 | |
| 			|| ((_align & Qt::AlignRight)
 | |
| 				&& (_quoteDirection == Qt::LeftToRight))) {
 | |
| 			_quoteShift = delta;
 | |
| 		}
 | |
| 		_x += _quoteShift;
 | |
| 	}
 | |
| 	_lineWidth = _startLineWidth
 | |
| 		- _quotePadding.left()
 | |
| 		- _quotePadding.right();
 | |
| 	_wLeft = _lineWidth;
 | |
| 	_elidedLine = line.elided;
 | |
| }
 | |
| 
 | |
| void Renderer::initParagraphBidi() {
 | |
| 	if (!_paragraphLength || !_paragraphAnalysis.isEmpty()) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	String::TextBlocks::const_iterator i = _paragraphStartBlock, e = _t->_blocks.cend(), n = i + 1;
 | |
| 
 | |
| 	bool ignore = false;
 | |
| 	bool rtl = (_paragraphDirection == Qt::RightToLeft);
 | |
| 	if (!ignore && !rtl) {
 | |
| 		ignore = true;
 | |
| 		const ushort *start = reinterpret_cast<const ushort*>(_str) + _paragraphStart;
 | |
| 		const ushort *curr = start;
 | |
| 		const ushort *end = start + _paragraphLength;
 | |
| 		while (curr < end) {
 | |
| 			while (n != e && (*n)->position() <= _paragraphStart + (curr - start)) {
 | |
| 				i = n;
 | |
| 				++n;
 | |
| 			}
 | |
| 			const auto type = (*i)->type();
 | |
| 			if (type != TextBlockType::Emoji
 | |
| 				&& type != TextBlockType::CustomEmoji
 | |
| 				&& *curr >= 0x590) {
 | |
| 				ignore = false;
 | |
| 				break;
 | |
| 			}
 | |
| 			++curr;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	_paragraphAnalysis.resize(_paragraphLength);
 | |
| 	QScriptAnalysis *analysis = _paragraphAnalysis.data();
 | |
| 
 | |
| 	BidiControl control(rtl);
 | |
| 
 | |
| 	_paragraphHasBidi = false;
 | |
| 	if (ignore) {
 | |
| 		memset(analysis, 0, _paragraphLength * sizeof(QScriptAnalysis));
 | |
| 		if (rtl) {
 | |
| 			for (int i = 0; i < _paragraphLength; ++i)
 | |
| 				analysis[i].bidiLevel = 1;
 | |
| 			_paragraphHasBidi = true;
 | |
| 		}
 | |
| 	} else {
 | |
| 		_paragraphHasBidi = eBidiItemize(analysis, control);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterator &_endBlockIter, const String::TextBlocks::const_iterator &_end) {
 | |
| 	_yDelta = (_lineHeight - _fontHeight) / 2;
 | |
| 	if (_yTo >= 0 && (_y + _yDelta >= _yTo || _y >= _yTo)) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	if (_y + _yDelta + _fontHeight <= _yFrom) {
 | |
| 		if (_lookupSymbol) {
 | |
| 			_lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart;
 | |
| 			_lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false;
 | |
| 		}
 | |
| 		return !_elidedLine;
 | |
| 	}
 | |
| 
 | |
| 	// Trimming pending spaces, because they sometimes don't fit on the line.
 | |
| 	// They also are not counted in the line width, they're in the right padding.
 | |
| 	// Line width is a sum of block / word widths and paddings between them, without trailing one.
 | |
| 	auto trimmedLineEnd = _lineEnd;
 | |
| 	for (; trimmedLineEnd > _lineStart; --trimmedLineEnd) {
 | |
| 		auto ch = _t->_text[trimmedLineEnd - 1];
 | |
| 		if (ch != QChar::Space && ch != QChar::LineFeed) {
 | |
| 			break;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	auto _endBlock = (_endBlockIter == _end) ? nullptr : _endBlockIter->get();
 | |
| 	if (_elidedLine) {
 | |
| 		// If we decided to draw the last line elided only because of the skip block
 | |
| 		// that did not fit on this line, we just draw the line till the very end.
 | |
| 		// Skip block is ignored in the elided lines, instead "removeFromEnd" is used.
 | |
| 		if (_endBlock && _endBlock->type() == TextBlockType::Skip) {
 | |
| 			_endBlock = nullptr;
 | |
| 		}
 | |
| 		if (!_endBlock) {
 | |
| 			_elidedLine = false;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	auto blockIndex = _lineStartBlock;
 | |
| 	auto currentBlock = _t->_blocks[blockIndex].get();
 | |
| 	auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
 | |
| 
 | |
| 	const auto extendLeft = (currentBlock->position() < _lineStart)
 | |
| 		? qMin(_lineStart - currentBlock->position(), 2)
 | |
| 		: 0;
 | |
| 	_localFrom = _lineStart - extendLeft;
 | |
| 	const auto extendedLineEnd = (_endBlock && _endBlock->position() < trimmedLineEnd && !_elidedLine)
 | |
| 		? qMin(uint16(trimmedLineEnd + 2), _t->countBlockEnd(_endBlockIter, _end))
 | |
| 		: trimmedLineEnd;
 | |
| 
 | |
| 	auto lineText = _t->_text.mid(_localFrom, extendedLineEnd - _localFrom);
 | |
| 	auto lineStart = extendLeft;
 | |
| 	auto lineLength = trimmedLineEnd - _lineStart;
 | |
| 
 | |
| 	if (_elidedLine) {
 | |
| 		initParagraphBidi();
 | |
| 		prepareElidedLine(lineText, lineStart, lineLength, _endBlock);
 | |
| 	}
 | |
| 
 | |
| 	auto x = _x;
 | |
| 	if (_align & Qt::AlignHCenter) {
 | |
| 		x += (_wLeft / 2).toInt();
 | |
| 	} else if (((_align & Qt::AlignLeft)
 | |
| 		&& (_paragraphDirection == Qt::RightToLeft))
 | |
| 		|| ((_align & Qt::AlignRight)
 | |
| 			&& (_paragraphDirection == Qt::LeftToRight))) {
 | |
| 		x += _wLeft;
 | |
| 	}
 | |
| 
 | |
| 	if (!_p) {
 | |
| 		if (_lookupX < x) {
 | |
| 			if (_lookupSymbol) {
 | |
| 				if (_paragraphDirection == Qt::RightToLeft) {
 | |
| 					_lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart;
 | |
| 					_lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false;
 | |
| 					//						_lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockType::Skip)) ? true : false;
 | |
| 				} else {
 | |
| 					_lookupResult.symbol = _lineStart;
 | |
| 					_lookupResult.afterSymbol = false;
 | |
| 					//						_lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineStart > 0)) ? true : false;
 | |
| 				}
 | |
| 			}
 | |
| 			if (_lookupLink) {
 | |
| 				_lookupResult.link = nullptr;
 | |
| 			}
 | |
| 			_lookupResult.uponSymbol = false;
 | |
| 			return false;
 | |
| 		} else if (_lookupX >= x + (_lineWidth - _wLeft)) {
 | |
| 			if (_paragraphDirection == Qt::RightToLeft) {
 | |
| 				_lookupResult.symbol = _lineStart;
 | |
| 				_lookupResult.afterSymbol = false;
 | |
| 				//					_lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineStart > 0)) ? true : false;
 | |
| 			} else {
 | |
| 				_lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart;
 | |
| 				_lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false;
 | |
| 				//					_lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockType::Skip)) ? true : false;
 | |
| 			}
 | |
| 			if (_lookupLink) {
 | |
| 				_lookupResult.link = nullptr;
 | |
| 			}
 | |
| 			_lookupResult.uponSymbol = false;
 | |
| 			return false;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (_fullWidthSelection) {
 | |
| 		const auto selectFromStart = (_selection.to > _lineStart)
 | |
| 			&& (_lineStart > 0)
 | |
| 			&& (_selection.from <= _lineStart);
 | |
| 		const auto selectTillEnd = (_selection.to > trimmedLineEnd)
 | |
| 			&& (trimmedLineEnd < _t->_text.size())
 | |
| 			&& (_selection.from <= trimmedLineEnd)
 | |
| 			&& (!_endBlock || _endBlock->type() != TextBlockType::Skip);
 | |
| 
 | |
| 		if ((selectFromStart && _paragraphDirection == Qt::LeftToRight)
 | |
| 			|| (selectTillEnd && _paragraphDirection == Qt::RightToLeft)) {
 | |
| 			if (x > _x) {
 | |
| 				fillSelectRange({ _x, x });
 | |
| 			}
 | |
| 		}
 | |
| 		if ((selectTillEnd && _paragraphDirection == Qt::LeftToRight)
 | |
| 			|| (selectFromStart && _paragraphDirection == Qt::RightToLeft)) {
 | |
| 			if (x < _x + _wLeft) {
 | |
| 				fillSelectRange({ x + _lineWidth - _wLeft, _x + _lineWidth });
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	if (trimmedLineEnd == _lineStart && !_elidedLine) {
 | |
| 		return true;
 | |
| 	}
 | |
| 
 | |
| 	if (!_elidedLine) {
 | |
| 		initParagraphBidi(); // if was not inited
 | |
| 	}
 | |
| 
 | |
| 	_f = _t->_st->font;
 | |
| 	QStackTextEngine engine(lineText, _f->f);
 | |
| 	engine.option.setTextDirection(_paragraphDirection);
 | |
| 	_e = &engine;
 | |
| 
 | |
| 	eItemize();
 | |
| 
 | |
| 	QScriptLine line;
 | |
| 	line.from = lineStart;
 | |
| 	line.length = lineLength;
 | |
| 	eShapeLine(line);
 | |
| 
 | |
| 	int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1);
 | |
| 	int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0;
 | |
| 	if (!nItems) {
 | |
| 		return !_elidedLine;
 | |
| 	}
 | |
| 
 | |
| 	int skipIndex = -1;
 | |
| 	QVarLengthArray<int> visualOrder(nItems);
 | |
| 	QVarLengthArray<uchar> levels(nItems);
 | |
| 	for (int i = 0; i < nItems; ++i) {
 | |
| 		auto &si = engine.layoutData->items[firstItem + i];
 | |
| 		while (nextBlock && nextBlock->position() <= _localFrom + si.position) {
 | |
| 			currentBlock = nextBlock;
 | |
| 			nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
 | |
| 		}
 | |
| 		auto _type = currentBlock->type();
 | |
| 		if (_type == TextBlockType::Skip) {
 | |
| 			levels[i] = si.analysis.bidiLevel = 0;
 | |
| 			skipIndex = i;
 | |
| 		} else {
 | |
| 			levels[i] = si.analysis.bidiLevel;
 | |
| 		}
 | |
| 		if (si.analysis.flags == QScriptAnalysis::Object) {
 | |
| 			if (_type == TextBlockType::Emoji
 | |
| 				|| _type == TextBlockType::CustomEmoji
 | |
| 				|| _type == TextBlockType::Skip) {
 | |
| 				si.width = currentBlock->f_width()
 | |
| 					+ (nextBlock == _endBlock && (!nextBlock || nextBlock->position() >= trimmedLineEnd)
 | |
| 						? 0
 | |
| 						: currentBlock->f_rpadding());
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	QTextEngine::bidiReorder(nItems, levels.data(), visualOrder.data());
 | |
| 	if (style::RightToLeft() && skipIndex == nItems - 1) {
 | |
| 		for (int32 i = nItems; i > 1;) {
 | |
| 			--i;
 | |
| 			visualOrder[i] = visualOrder[i - 1];
 | |
| 		}
 | |
| 		visualOrder[0] = skipIndex;
 | |
| 	}
 | |
| 
 | |
| 	blockIndex = _lineStartBlock;
 | |
| 	currentBlock = _t->_blocks[blockIndex].get();
 | |
| 	nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
 | |
| 
 | |
| 	int32 textY = _y + _yDelta + _t->_st->font->ascent, emojiY = (_t->_st->font->height - st::emojiSize) / 2;
 | |
| 
 | |
| 	applyBlockProperties(currentBlock);
 | |
| 	for (int i = 0; i < nItems; ++i) {
 | |
| 		const auto item = firstItem + visualOrder[i];
 | |
| 		const auto isLastItem = (item == lastItem);
 | |
| 		const auto &si = engine.layoutData->items.at(item);
 | |
| 		const auto rtl = (si.analysis.bidiLevel % 2);
 | |
| 
 | |
| 		while (blockIndex > _lineStartBlock + 1 && _t->_blocks[blockIndex - 1]->position() > _localFrom + si.position) {
 | |
| 			nextBlock = currentBlock;
 | |
| 			currentBlock = _t->_blocks[--blockIndex - 1].get();
 | |
| 			applyBlockProperties(currentBlock);
 | |
| 		}
 | |
| 		while (nextBlock && nextBlock->position() <= _localFrom + si.position) {
 | |
| 			currentBlock = nextBlock;
 | |
| 			nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
 | |
| 			applyBlockProperties(currentBlock);
 | |
| 		}
 | |
| 		if (si.analysis.flags >= QScriptAnalysis::TabOrObject) {
 | |
| 			TextBlockType _type = currentBlock->type();
 | |
| 			if (!_p && _lookupX >= x && _lookupX < x + si.width) { // _lookupRequest
 | |
| 				if (_lookupLink) {
 | |
| 					if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
 | |
| 						if (const auto link = lookupLink(currentBlock)) {
 | |
| 							_lookupResult.link = link;
 | |
| 						}
 | |
| 					}
 | |
| 				}
 | |
| 				if (_type != TextBlockType::Skip) {
 | |
| 					_lookupResult.uponSymbol = true;
 | |
| 				}
 | |
| 				if (_lookupSymbol) {
 | |
| 					if (_type == TextBlockType::Skip) {
 | |
| 						if (_paragraphDirection == Qt::RightToLeft) {
 | |
| 							_lookupResult.symbol = _lineStart;
 | |
| 							_lookupResult.afterSymbol = false;
 | |
| 						} else {
 | |
| 							_lookupResult.symbol = (trimmedLineEnd > _lineStart) ? (trimmedLineEnd - 1) : _lineStart;
 | |
| 							_lookupResult.afterSymbol = (trimmedLineEnd > _lineStart) ? true : false;
 | |
| 						}
 | |
| 						return false;
 | |
| 					}
 | |
| 
 | |
| 					// Emoji with spaces after symbol lookup
 | |
| 					auto chFrom = _str + currentBlock->position();
 | |
| 					auto chTo = chFrom + ((nextBlock ? nextBlock->position() : _t->_text.size()) - currentBlock->position());
 | |
| 					auto spacesWidth = (si.width - currentBlock->f_width());
 | |
| 					auto spacesCount = 0;
 | |
| 					while (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space) {
 | |
| 						++spacesCount;
 | |
| 						--chTo;
 | |
| 					}
 | |
| 					if (spacesCount > 0) { // Check if we're over a space.
 | |
| 						if (rtl) {
 | |
| 							if (_lookupX < x + spacesWidth) {
 | |
| 								_lookupResult.symbol = (chTo - _str); // up to a space, included, rtl
 | |
| 								_lookupResult.afterSymbol = (_lookupX < x + (spacesWidth / 2)) ? true : false;
 | |
| 								return false;
 | |
| 							}
 | |
| 						} else if (_lookupX >= x + si.width - spacesWidth) {
 | |
| 							_lookupResult.symbol = (chTo - _str); // up to a space, inclided, ltr
 | |
| 							_lookupResult.afterSymbol = (_lookupX >= x + si.width - spacesWidth + (spacesWidth / 2)) ? true : false;
 | |
| 							return false;
 | |
| 						}
 | |
| 					}
 | |
| 					if (_lookupX < x + (rtl ? (si.width - currentBlock->f_width()) : 0) + (currentBlock->f_width() / 2)) {
 | |
| 						_lookupResult.symbol = ((rtl && chTo > chFrom) ? (chTo - 1) : chFrom) - _str;
 | |
| 						_lookupResult.afterSymbol = (rtl && chTo > chFrom) ? true : false;
 | |
| 					} else {
 | |
| 						_lookupResult.symbol = ((rtl || chTo <= chFrom) ? chFrom : (chTo - 1)) - _str;
 | |
| 						_lookupResult.afterSymbol = (rtl || chTo <= chFrom) ? false : true;
 | |
| 					}
 | |
| 				}
 | |
| 				return false;
 | |
| 			} else if (_p
 | |
| 				&& (_type == TextBlockType::Emoji
 | |
| 					|| _type == TextBlockType::CustomEmoji)) {
 | |
| 				auto glyphX = x;
 | |
| 				auto spacesWidth = (si.width - currentBlock->f_width());
 | |
| 				if (rtl) {
 | |
| 					glyphX += spacesWidth;
 | |
| 				}
 | |
| 				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.);
 | |
| 				const auto fillSpoiler = hasSpoiler
 | |
| 					? FixedRange{ x, x + si.width }
 | |
| 					: FixedRange();
 | |
| 				const auto opacity = _p->opacity();
 | |
| 				if (!hasSpoiler || _spoilerOpacity < 1.) {
 | |
| 					if (hasSpoiler) {
 | |
| 						_p->setOpacity(opacity * (1. - _spoilerOpacity));
 | |
| 					}
 | |
| 					const auto x = (glyphX + st::emojiPadding).toInt();
 | |
| 					const auto y = _y + _yDelta + emojiY;
 | |
| 					if (_type == TextBlockType::Emoji) {
 | |
| 						Emoji::Draw(
 | |
| 							*_p,
 | |
| 							static_cast<const EmojiBlock*>(currentBlock)->_emoji,
 | |
| 							Emoji::GetSizeNormal(),
 | |
| 							x,
 | |
| 							y);
 | |
| 					} else if (const auto custom = static_cast<const CustomEmojiBlock*>(currentBlock)->_custom.get()) {
 | |
| 						const auto selected = (fillSelect.from <= glyphX)
 | |
| 							&& (fillSelect.till > glyphX);
 | |
| 						const auto color = (selected
 | |
| 							? _currentPenSelected
 | |
| 							: _currentPen)->color();
 | |
| 						if (!_customEmojiContext) {
 | |
| 							_customEmojiContext = CustomEmoji::Context{
 | |
| 								.textColor = color,
 | |
| 								.now = now(),
 | |
| 								.paused = _pausedEmoji,
 | |
| 							};
 | |
| 							_customEmojiSkip = (st::emojiSize
 | |
| 								- AdjustCustomEmojiSize(st::emojiSize)) / 2;
 | |
| 						} else {
 | |
| 							_customEmojiContext->textColor = color;
 | |
| 						}
 | |
| 						_customEmojiContext->position = {
 | |
| 							x + _customEmojiSkip,
 | |
| 							y + _customEmojiSkip,
 | |
| 						};
 | |
| 						custom->paint(*_p, *_customEmojiContext);
 | |
| 					}
 | |
| 					if (hasSpoiler) {
 | |
| 						_p->setOpacity(opacity);
 | |
| 					}
 | |
| 				}
 | |
| 				if (hasSpoiler) {
 | |
| 					// Elided item should be a text item
 | |
| 					// with '...' at the end, so this should not be it.
 | |
| 					const auto isElidedItem = false;
 | |
| 					pushSpoilerRange(fillSpoiler, fillSelect, isElidedItem);
 | |
| 				}
 | |
| 			//} 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));
 | |
| 			}
 | |
| 			x += si.width;
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		unsigned short *logClusters = engine.logClusters(&si);
 | |
| 		QGlyphLayout glyphs = engine.shapedGlyphs(&si);
 | |
| 
 | |
| 		int itemStart = qMax(line.from, si.position), itemEnd;
 | |
| 		int itemLength = engine.length(item);
 | |
| 		int glyphsStart = logClusters[itemStart - si.position], glyphsEnd;
 | |
| 		if (line.from + line.length < si.position + itemLength) {
 | |
| 			itemEnd = line.from + line.length;
 | |
| 			glyphsEnd = logClusters[itemEnd - si.position];
 | |
| 		} else {
 | |
| 			itemEnd = si.position + itemLength;
 | |
| 			glyphsEnd = si.num_glyphs;
 | |
| 		}
 | |
| 
 | |
| 		QFixed itemWidth = 0;
 | |
| 		for (int g = glyphsStart; g < glyphsEnd; ++g)
 | |
| 			itemWidth += glyphs.effectiveAdvance(g);
 | |
| 
 | |
| 		if (!_p && _lookupX >= x && _lookupX < x + itemWidth) { // _lookupRequest
 | |
| 			if (_lookupLink) {
 | |
| 				if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
 | |
| 					if (const auto link = lookupLink(currentBlock)) {
 | |
| 						_lookupResult.link = link;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 			_lookupResult.uponSymbol = true;
 | |
| 			if (_lookupSymbol) {
 | |
| 				QFixed tmpx = rtl ? (x + itemWidth) : x;
 | |
| 				for (int ch = 0, g, itemL = itemEnd - itemStart; ch < itemL;) {
 | |
| 					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;
 | |
| 					}
 | |
| 					for (int charsCount = (ch2 - ch); ch < ch2; ++ch) {
 | |
| 						QFixed shift1 = QFixed(2 * (charsCount - (ch2 - ch)) + 2) * gwidth / QFixed(2 * charsCount),
 | |
| 							shift2 = QFixed(2 * (charsCount - (ch2 - ch)) + 1) * gwidth / QFixed(2 * charsCount);
 | |
| 						if ((rtl && _lookupX >= tmpx - shift1) ||
 | |
| 							(!rtl && _lookupX < tmpx + shift1)) {
 | |
| 							_lookupResult.symbol = _localFrom + itemStart + ch;
 | |
| 							if ((rtl && _lookupX >= tmpx - shift2) ||
 | |
| 								(!rtl && _lookupX < tmpx + shift2)) {
 | |
| 								_lookupResult.afterSymbol = false;
 | |
| 							} else {
 | |
| 								_lookupResult.afterSymbol = true;
 | |
| 							}
 | |
| 							return false;
 | |
| 						}
 | |
| 					}
 | |
| 					if (rtl) {
 | |
| 						tmpx -= gwidth;
 | |
| 					} else {
 | |
| 						tmpx += gwidth;
 | |
| 					}
 | |
| 				}
 | |
| 				if (itemEnd > itemStart) {
 | |
| 					_lookupResult.symbol = _localFrom + itemEnd - 1;
 | |
| 					_lookupResult.afterSymbol = true;
 | |
| 				} else {
 | |
| 					_lookupResult.symbol = _localFrom + itemStart;
 | |
| 					_lookupResult.afterSymbol = false;
 | |
| 				}
 | |
| 			}
 | |
| 			return false;
 | |
| 		} else if (_p) {
 | |
| 			QTextItemInt gf;
 | |
| 			gf.glyphs = glyphs.mid(glyphsStart, glyphsEnd - glyphsStart);
 | |
| 			gf.f = &_e->fnt;
 | |
| 			gf.chars = engine.layoutData->string.unicode() + itemStart;
 | |
| 			gf.num_chars = itemEnd - itemStart;
 | |
| 			gf.fontEngine = engine.fontEngine(si);
 | |
| 			gf.logClusters = logClusters + itemStart - si.position;
 | |
| 			gf.width = itemWidth;
 | |
| 			gf.justified = false;
 | |
| 			InitTextItemWithScriptItem(gf, si);
 | |
| 
 | |
| 			const auto itemRange = FixedRange{ x, x + itemWidth };
 | |
| 			auto selectedRect = QRect();
 | |
| 			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.);
 | |
| 			const auto opacity = _p->opacity();
 | |
| 			const auto isElidedItem = (_indexOfElidedBlock == blockIndex)
 | |
| 				&& isLastItem;
 | |
| 			const auto complexClipping = hasSpoiler
 | |
| 				&& isElidedItem
 | |
| 				&& (_spoilerOpacity == 1.);
 | |
| 			if (!hasSpoiler || (_spoilerOpacity < 1.) || isElidedItem) {
 | |
| 				const auto complexClippingEnabled = complexClipping
 | |
| 					&& _p->hasClipping();
 | |
| 				const auto complexClippingRegion = complexClipping
 | |
| 					? _p->clipRegion()
 | |
| 					: QRegion();
 | |
| 				if (complexClipping) {
 | |
| 					const auto elided = (_indexOfElidedBlock == blockIndex)
 | |
| 						? _f->elidew
 | |
| 						: 0;
 | |
| 					_p->setClipRect(
 | |
| 						QRect(
 | |
| 							(x + itemWidth).toInt() - elided,
 | |
| 							_y - _lineHeight,
 | |
| 							elided,
 | |
| 							_y + 2 * _lineHeight),
 | |
| 						Qt::IntersectClip);
 | |
| 				} else if (hasSpoiler && !isElidedItem) {
 | |
| 					_p->setOpacity(opacity * (1. - _spoilerOpacity));
 | |
| 				}
 | |
| 				if (Q_UNLIKELY(hasSelected)) {
 | |
| 					if (Q_UNLIKELY(hasNotSelected)) {
 | |
| 						// There is a bug in retina QPainter clipping stack.
 | |
| 						// You can see glitches in rendering in such text:
 | |
| 						// aA
 | |
| 						// Aa
 | |
| 						// Where selection is both 'A'-s.
 | |
| 						// I can't debug it right now, this is a workaround.
 | |
| #ifdef Q_OS_MAC
 | |
| 						_p->save();
 | |
| #endif // Q_OS_MAC
 | |
| 						const auto clippingEnabled = _p->hasClipping();
 | |
| 						const auto clippingRegion = _p->clipRegion();
 | |
| 						_p->setClipRect(selectedRect, Qt::IntersectClip);
 | |
| 						_p->setPen(*_currentPenSelected);
 | |
| 						_p->drawTextItem(QPointF(x.toReal(), textY), gf);
 | |
| 						const auto externalClipping = clippingEnabled
 | |
| 							? clippingRegion
 | |
| 							: QRegion(QRect(
 | |
| 								(_x - _lineWidth).toInt(),
 | |
| 								_y - _lineHeight,
 | |
| 								(_x + 2 * _lineWidth).toInt(),
 | |
| 								_y + 2 * _lineHeight));
 | |
| 						_p->setClipRegion(externalClipping - selectedRect);
 | |
| 						_p->setPen(*_currentPen);
 | |
| 						_p->drawTextItem(QPointF(x.toReal(), textY), gf);
 | |
| #ifdef Q_OS_MAC
 | |
| 						_p->restore();
 | |
| #else // Q_OS_MAC
 | |
| 						if (clippingEnabled) {
 | |
| 							_p->setClipRegion(clippingRegion);
 | |
| 						} else {
 | |
| 							_p->setClipping(false);
 | |
| 						}
 | |
| #endif // Q_OS_MAC
 | |
| 					} else {
 | |
| 						_p->setPen(*_currentPenSelected);
 | |
| 						_p->drawTextItem(QPointF(x.toReal(), textY), gf);
 | |
| 					}
 | |
| 				} else {
 | |
| 					_p->setPen(*_currentPen);
 | |
| 					_p->drawTextItem(QPointF(x.toReal(), textY), gf);
 | |
| 				}
 | |
| 				if (complexClipping) {
 | |
| 					if (complexClippingEnabled) {
 | |
| 						_p->setClipRegion(complexClippingRegion);
 | |
| 					} else {
 | |
| 						_p->setClipping(false);
 | |
| 					}
 | |
| 				} else if (hasSpoiler && !isElidedItem) {
 | |
| 					_p->setOpacity(opacity);
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if (hasSpoiler) {
 | |
| 				pushSpoilerRange(itemRange, fillSelect, isElidedItem);
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		x += itemWidth;
 | |
| 	}
 | |
| 	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;
 | |
| 	}
 | |
| 	const auto left = range.from.toInt();
 | |
| 	const auto width = range.till.toInt() - left;
 | |
| 	_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,
 | |
| 		bool isElidedItem) {
 | |
| 	if (!_background.spoiler || !_spoiler) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto elided = isElidedItem ? _f->elidew : 0;
 | |
| 	range.till -= elided;
 | |
| 	if (range.empty()) {
 | |
| 		return;
 | |
| 	} else if (selected.empty() || !Intersects(range, selected)) {
 | |
| 		AppendRange(_spoilerRanges, range);
 | |
| 	} else {
 | |
| 		AppendRange(_spoilerRanges, { range.from, selected.from });
 | |
| 		AppendRange(_spoilerSelectedRanges, Intersected(range, selected));
 | |
| 		AppendRange(_spoilerRanges, { selected.till, range.till });
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Renderer::fillRectsFromRanges() {
 | |
| 	fillRectsFromRanges(_spoilerRects, _spoilerRanges);
 | |
| 	fillRectsFromRanges(_spoilerSelectedRects, _spoilerSelectedRanges);
 | |
| 	fillRectsFromRanges(_highlightRects, _highlightRanges);
 | |
| }
 | |
| 
 | |
| void Renderer::fillRectsFromRanges(
 | |
| 		QVarLengthArray<QRect, kSpoilersRectsSize> &rects,
 | |
| 		QVarLengthArray<FixedRange> &ranges) {
 | |
| 	if (ranges.empty()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	auto lastTill = ranges.front().from.toInt() - 1;
 | |
| 	const auto y = _y + _yDelta;
 | |
| 	for (const auto &range : ranges) {
 | |
| 		auto from = range.from.toInt();
 | |
| 		auto till = range.till.toInt();
 | |
| 		if (from <= lastTill) {
 | |
| 			auto &last = rects.back();
 | |
| 			from = std::min(from, last.x());
 | |
| 			till = std::max(till, last.x() + last.width());
 | |
| 			last = { from, y, till - from, _fontHeight };
 | |
| 		} else {
 | |
| 			rects.push_back({ from, y, till - from, _fontHeight });
 | |
| 		}
 | |
| 		lastTill = till;
 | |
| 	}
 | |
| 	ranges.clear();
 | |
| }
 | |
| 
 | |
| void Renderer::paintSpoilerRects() {
 | |
| 	Expects(_p != nullptr);
 | |
| 
 | |
| 	if (!_spoiler) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto opacity = _p->opacity();
 | |
| 	if (_spoilerOpacity < 1.) {
 | |
| 		_p->setOpacity(opacity * _spoilerOpacity);
 | |
| 	}
 | |
| 	const auto index = _spoiler->animation.index(now(), _pausedSpoiler);
 | |
| 	paintSpoilerRects(
 | |
| 		_spoilerRects,
 | |
| 		_palette->spoilerFg,
 | |
| 		index);
 | |
| 	paintSpoilerRects(
 | |
| 		_spoilerSelectedRects,
 | |
| 		_palette->selectSpoilerFg,
 | |
| 		index);
 | |
| 	if (_spoilerOpacity < 1.) {
 | |
| 		_p->setOpacity(opacity);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Renderer::paintSpoilerRects(
 | |
| 		const QVarLengthArray<QRect, kSpoilersRectsSize> &rects,
 | |
| 		const style::color &color,
 | |
| 		int index) {
 | |
| 	if (rects.empty()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto frame = _spoilerCache->lookup(color->c)->frame(index);
 | |
| 	if (_spoilerCache) {
 | |
| 		for (const auto &rect : rects) {
 | |
| 			Ui::FillSpoilerRect(*_p, rect, frame, -rect.topLeft());
 | |
| 		}
 | |
| 	} else {
 | |
| 		// Show forgotten spoiler context part.
 | |
| 		for (const auto &rect : rects) {
 | |
| 			_p->fillRect(rect, Qt::red);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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();
 | |
| 	}
 | |
| 
 | |
| 	_elideSavedIndex = blockIndex;
 | |
| 	auto mutableText = const_cast<String*>(_t);
 | |
| 	_elideSavedBlock = std::move(mutableText->_blocks[blockIndex]);
 | |
| 	mutableText->_blocks[blockIndex] = Block::Text(
 | |
| 		_t->_st->font,
 | |
| 		_t->_text,
 | |
| 		elideStart, 0,
 | |
| 		(*_elideSavedBlock)->flags(),
 | |
| 		(*_elideSavedBlock)->linkIndex(),
 | |
| 		(*_elideSavedBlock)->colorIndex(),
 | |
| 		QFIXED_MAX);
 | |
| 	_blocksSize = blockIndex + 1;
 | |
| 	_endBlock = (blockIndex + 1 < _t->_blocks.size())
 | |
| 		? _t->_blocks[blockIndex + 1].get()
 | |
| 		: nullptr;
 | |
| }
 | |
| 
 | |
| void Renderer::setElideBidi(int32 elideStart, int32 elideLen) {
 | |
| 	int32 newParLength = elideStart + elideLen - _paragraphStart;
 | |
| 	if (newParLength > _paragraphAnalysis.size()) {
 | |
| 		_paragraphAnalysis.resize(newParLength);
 | |
| 	}
 | |
| 	for (int32 i = elideLen; i > 0; --i) {
 | |
| 		_paragraphAnalysis[newParLength - i].bidiLevel
 | |
| 			= (_paragraphDirection == Qt::RightToLeft) ? 1 : 0;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Renderer::prepareElidedLine(
 | |
| 		QString &lineText,
 | |
| 		int32 lineStart,
 | |
| 		int32 &lineLength,
 | |
| 		const AbstractBlock *&_endBlock,
 | |
| 		int repeat) {
 | |
| 	_f = _t->_st->font;
 | |
| 	QStackTextEngine engine(lineText, _f->f);
 | |
| 	engine.option.setTextDirection(_paragraphDirection);
 | |
| 	_e = &engine;
 | |
| 
 | |
| 	eItemize();
 | |
| 
 | |
| 	auto blockIndex = _lineStartBlock;
 | |
| 	auto currentBlock = _t->_blocks[blockIndex].get();
 | |
| 	auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
 | |
| 
 | |
| 	QScriptLine line;
 | |
| 	line.from = lineStart;
 | |
| 	line.length = lineLength;
 | |
| 	eShapeLine(line);
 | |
| 
 | |
| 	auto elideWidth = _f->elidew;
 | |
| 	_wLeft = _lineWidth
 | |
| 		- _quotePadding.left()
 | |
| 		- _quotePadding.right()
 | |
| 		- elideWidth;
 | |
| 
 | |
| 	int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1);
 | |
| 	int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0, i;
 | |
| 
 | |
| 	for (i = 0; i < nItems; ++i) {
 | |
| 		QScriptItem &si(engine.layoutData->items[firstItem + i]);
 | |
| 		while (nextBlock && nextBlock->position() <= _localFrom + si.position) {
 | |
| 			currentBlock = nextBlock;
 | |
| 			nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
 | |
| 		}
 | |
| 		TextBlockType _type = currentBlock->type();
 | |
| 		if (si.analysis.flags == QScriptAnalysis::Object) {
 | |
| 			if (_type == TextBlockType::Emoji
 | |
| 				|| _type == TextBlockType::CustomEmoji
 | |
| 				|| _type == TextBlockType::Skip) {
 | |
| 				si.width = currentBlock->f_width() + currentBlock->f_rpadding();
 | |
| 			}
 | |
| 		}
 | |
| 		if (_type == TextBlockType::Emoji
 | |
| 			|| _type == TextBlockType::CustomEmoji
 | |
| 			|| _type == TextBlockType::Skip
 | |
| 			|| _type == TextBlockType::Newline) {
 | |
| 			if (_wLeft < si.width) {
 | |
| 				lineText = lineText.mid(0, currentBlock->position() - _localFrom) + kQEllipsis;
 | |
| 				lineLength = currentBlock->position() + kQEllipsis.size() - _lineStart;
 | |
| 				_selection.to = qMin(_selection.to, currentBlock->position());
 | |
| 				_indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0);
 | |
| 				setElideBidi(currentBlock->position(), kQEllipsis.size());
 | |
| 				elideSaveBlock(blockIndex - 1, _endBlock, currentBlock->position(), elideWidth);
 | |
| 				return;
 | |
| 			}
 | |
| 			_wLeft -= si.width;
 | |
| 		} else if (_type == TextBlockType::Text) {
 | |
| 			unsigned short *logClusters = engine.logClusters(&si);
 | |
| 			QGlyphLayout glyphs = engine.shapedGlyphs(&si);
 | |
| 
 | |
| 			int itemStart = qMax(line.from, si.position), itemEnd;
 | |
| 			int itemLength = engine.length(firstItem + i);
 | |
| 			int glyphsStart = logClusters[itemStart - si.position], glyphsEnd;
 | |
| 			if (line.from + line.length < si.position + itemLength) {
 | |
| 				itemEnd = line.from + line.length;
 | |
| 				glyphsEnd = logClusters[itemEnd - si.position];
 | |
| 			} else {
 | |
| 				itemEnd = si.position + itemLength;
 | |
| 				glyphsEnd = si.num_glyphs;
 | |
| 			}
 | |
| 
 | |
| 			for (auto g = glyphsStart; g < glyphsEnd; ++g) {
 | |
| 				auto adv = glyphs.effectiveAdvance(g);
 | |
| 				if (_wLeft < adv) {
 | |
| 					auto pos = itemStart;
 | |
| 					while (pos < itemEnd && logClusters[pos - si.position] < g) {
 | |
| 						++pos;
 | |
| 					}
 | |
| 
 | |
| 					if (lineText.size() <= pos || repeat > 3) {
 | |
| 						lineText += kQEllipsis;
 | |
| 						lineLength = _localFrom + pos + kQEllipsis.size() - _lineStart;
 | |
| 						_selection.to = qMin(_selection.to, uint16(_localFrom + pos));
 | |
| 						_indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0);
 | |
| 						setElideBidi(_localFrom + pos, kQEllipsis.size());
 | |
| 						_blocksSize = blockIndex;
 | |
| 						_endBlock = nextBlock;
 | |
| 					} else {
 | |
| 						lineText = lineText.mid(0, pos);
 | |
| 						lineLength = _localFrom + pos - _lineStart;
 | |
| 						_blocksSize = blockIndex;
 | |
| 						_endBlock = nextBlock;
 | |
| 						prepareElidedLine(lineText, lineStart, lineLength, _endBlock, repeat + 1);
 | |
| 					}
 | |
| 					return;
 | |
| 				} else {
 | |
| 					_wLeft -= adv;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	int32 elideStart = _localFrom + lineText.size();
 | |
| 	_selection.to = qMin(_selection.to, uint16(elideStart));
 | |
| 	_indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0);
 | |
| 	setElideBidi(elideStart, kQEllipsis.size());
 | |
| 
 | |
| 	lineText += kQEllipsis;
 | |
| 	lineLength += kQEllipsis.size();
 | |
| 
 | |
| 	if (!repeat) {
 | |
| 		for (; blockIndex < _blocksSize && _t->_blocks[blockIndex].get() != _endBlock && _t->_blocks[blockIndex]->position() < elideStart; ++blockIndex) {
 | |
| 		}
 | |
| 		if (blockIndex < _blocksSize) {
 | |
| 			elideSaveBlock(blockIndex, _endBlock, elideStart, elideWidth);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Renderer::restoreAfterElided() {
 | |
| 	if (_elideSavedBlock) {
 | |
| 		const_cast<String*>(_t)->_blocks[_elideSavedIndex] = std::move(*_elideSavedBlock);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // COPIED FROM qtextengine.cpp AND MODIFIED
 | |
| void Renderer::eAppendItems(QScriptAnalysis *analysis, int &start, int &stop, const BidiControl &control, QChar::Direction dir) {
 | |
| 	if (start > stop)
 | |
| 		return;
 | |
| 
 | |
| 	int level = control.level;
 | |
| 
 | |
| 	if(dir != QChar::DirON && !control.override) {
 | |
| 		// add level of run (cases I1 & I2)
 | |
| 		if(level % 2) {
 | |
| 			if(dir == QChar::DirL || dir == QChar::DirAN || dir == QChar::DirEN)
 | |
| 				level++;
 | |
| 		} else {
 | |
| 			if(dir == QChar::DirR)
 | |
| 				level++;
 | |
| 			else if(dir == QChar::DirAN || dir == QChar::DirEN)
 | |
| 				level += 2;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	QScriptAnalysis *s = analysis + start;
 | |
| 	const QScriptAnalysis *e = analysis + stop;
 | |
| 	while (s <= e) {
 | |
| 		s->bidiLevel = level;
 | |
| 		++s;
 | |
| 	}
 | |
| 	++stop;
 | |
| 	start = stop;
 | |
| }
 | |
| 
 | |
| void Renderer::eShapeLine(const QScriptLine &line) {
 | |
| 	int item = _e->findItem(line.from);
 | |
| 	if (item == -1) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	auto end = _e->findItem(line.from + line.length - 1, item);
 | |
| 	auto blockIndex = _lineStartBlock;
 | |
| 	auto currentBlock = _t->_blocks[blockIndex].get();
 | |
| 	auto nextBlock = (++blockIndex < _blocksSize)
 | |
| 		? _t->_blocks[blockIndex].get()
 | |
| 		: nullptr;
 | |
| 	eSetFont(currentBlock);
 | |
| 	for (; item <= end; ++item) {
 | |
| 		QScriptItem &si = _e->layoutData->items[item];
 | |
| 		while (nextBlock && nextBlock->position() <= _localFrom + si.position) {
 | |
| 			currentBlock = nextBlock;
 | |
| 			nextBlock = (++blockIndex < _blocksSize)
 | |
| 				? _t->_blocks[blockIndex].get()
 | |
| 				: nullptr;
 | |
| 			eSetFont(currentBlock);
 | |
| 		}
 | |
| 		_e->shape(item);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Renderer::eSetFont(const AbstractBlock *block) {
 | |
| 	const auto flags = block->flags();
 | |
| 	const auto usedFont = [&] {
 | |
| 		if (const auto index = block->linkIndex()) {
 | |
| 			const auto underline = _t->_st->linkUnderline;
 | |
| 			const auto underlined = (underline == st::kLinkUnderlineNever)
 | |
| 				? false
 | |
| 				: (underline == st::kLinkUnderlineActive)
 | |
| 				? ((_palette && _palette->linkAlwaysActive)
 | |
| 					|| ClickHandler::showAsActive(_t->_extended
 | |
| 						? _t->_extended->links[index - 1]
 | |
| 						: nullptr))
 | |
| 				: true;
 | |
| 			return underlined ? _t->_st->font->underline() : _t->_st->font;
 | |
| 		}
 | |
| 		return _t->_st->font;
 | |
| 	}();
 | |
| 	const auto newFont = WithFlags(usedFont, flags);
 | |
| 	if (newFont != _f) {
 | |
| 		_f = (newFont->family() == _t->_st->font->family())
 | |
| 			? WithFlags(_t->_st->font, flags, newFont->flags())
 | |
| 			: newFont;
 | |
| 		_e->fnt = _f->f;
 | |
| 		_e->resetFontEngineCache();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Renderer::eItemize() {
 | |
| 	_e->validate();
 | |
| 	if (_e->layoutData->items.size())
 | |
| 		return;
 | |
| 
 | |
| 	int length = _e->layoutData->string.length();
 | |
| 	if (!length)
 | |
| 		return;
 | |
| 
 | |
| 	const ushort *string = reinterpret_cast<const ushort*>(_e->layoutData->string.unicode());
 | |
| 
 | |
| 	auto blockIndex = _lineStartBlock;
 | |
| 	auto currentBlock = _t->_blocks[blockIndex].get();
 | |
| 	auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
 | |
| 
 | |
| 	_e->layoutData->hasBidi = _paragraphHasBidi;
 | |
| 	auto analysis = _paragraphAnalysis.data() + (_localFrom - _paragraphStart);
 | |
| 
 | |
| 	{
 | |
| #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
 | |
| 		QUnicodeTools::ScriptItemArray scriptItems;
 | |
| 		QUnicodeTools::initScripts(_e->layoutData->string, &scriptItems);
 | |
| 		for (int i = 0; i < scriptItems.length(); ++i) {
 | |
| 			const auto &item = scriptItems.at(i);
 | |
| 			int end = i < scriptItems.length() - 1 ? scriptItems.at(i + 1).position : length;
 | |
| 			for (int j = item.position; j < end; ++j)
 | |
| 				analysis[j].script = item.script;
 | |
| 		}
 | |
| #else // Qt >= 6.0.0
 | |
| 		QVarLengthArray<uchar> scripts(length);
 | |
| 		QUnicodeTools::initScripts(string, length, scripts.data());
 | |
| 		for (int i = 0; i < length; ++i)
 | |
| 			analysis[i].script = scripts.at(i);
 | |
| #endif // Qt < 6.0.0
 | |
| 	}
 | |
| 
 | |
| 	blockIndex = _lineStartBlock;
 | |
| 	currentBlock = _t->_blocks[blockIndex].get();
 | |
| 	nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
 | |
| 
 | |
| 	auto start = string;
 | |
| 	auto end = start + length;
 | |
| 	while (start < end) {
 | |
| 		while (nextBlock && nextBlock->position() <= _localFrom + (start - string)) {
 | |
| 			currentBlock = nextBlock;
 | |
| 			nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
 | |
| 		}
 | |
| 		auto _type = currentBlock->type();
 | |
| 		if (_type == TextBlockType::Emoji
 | |
| 			|| _type == TextBlockType::CustomEmoji
 | |
| 			|| _type == TextBlockType::Skip) {
 | |
| 			analysis->script = QChar::Script_Common;
 | |
| 			analysis->flags = QScriptAnalysis::Object;
 | |
| 		} else {
 | |
| 			analysis->flags = QScriptAnalysis::None;
 | |
| 		}
 | |
| #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
 | |
| 		analysis->script = hbscript_to_script(script_to_hbscript(analysis->script)); // retain the old behavior
 | |
| #endif // Qt < 6.0.0
 | |
| 		++start;
 | |
| 		++analysis;
 | |
| 	}
 | |
| 
 | |
| 	{
 | |
| 		auto i_string = &_e->layoutData->string;
 | |
| 		auto i_analysis = _paragraphAnalysis.data() + (_localFrom - _paragraphStart);
 | |
| 		auto i_items = &_e->layoutData->items;
 | |
| 
 | |
| 		blockIndex = _lineStartBlock;
 | |
| 		currentBlock = _t->_blocks[blockIndex].get();
 | |
| 		nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
 | |
| 		auto startBlock = currentBlock;
 | |
| 
 | |
| 		if (!length) {
 | |
| 			return;
 | |
| 		}
 | |
| 		auto start = 0;
 | |
| 		auto end = start + length;
 | |
| 		for (int i = start + 1; i < end; ++i) {
 | |
| 			while (nextBlock && nextBlock->position() <= _localFrom + i) {
 | |
| 				currentBlock = nextBlock;
 | |
| 				nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
 | |
| 			}
 | |
| 			// According to the unicode spec we should be treating characters in the Common script
 | |
| 			// (punctuation, spaces, etc) as being the same script as the surrounding text for the
 | |
| 			// purpose of splitting up text. This is important because, for example, a fullstop
 | |
| 			// (0x2E) can be used to indicate an abbreviation and so must be treated as part of a
 | |
| 			// word.  Thus it must be passed along with the word in languages that have to calculate
 | |
| 			// word breaks. For example the thai word "[lookup-in-git]." has no word breaks
 | |
| 			// but the word "[lookup-too]" does.
 | |
| 			// Unfortuntely because we split up the strings for both wordwrapping and for setting
 | |
| 			// the font and because Japanese and Chinese are also aliases of the script "Common",
 | |
| 			// doing this would break too many things.  So instead we only pass the full stop
 | |
| 			// along, and nothing else.
 | |
| 			if (currentBlock == startBlock
 | |
| 				&& i_analysis[i].bidiLevel == i_analysis[start].bidiLevel
 | |
| 				&& i_analysis[i].flags == i_analysis[start].flags
 | |
| 				&& (i_analysis[i].script == i_analysis[start].script || i_string->at(i) == QLatin1Char('.'))
 | |
| 				//					&& i_analysis[i].flags < QScriptAnalysis::SpaceTabOrObject // only emojis are objects here, no tabs
 | |
| 				&& i - start < _MaxItemLength)
 | |
| 				continue;
 | |
| 			i_items->append(QScriptItem(start, i_analysis[start]));
 | |
| 			start = i;
 | |
| 			startBlock = currentBlock;
 | |
| 		}
 | |
| 		i_items->append(QScriptItem(start, i_analysis[start]));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| QChar::Direction Renderer::eSkipBoundryNeutrals(
 | |
| 		QScriptAnalysis *analysis,
 | |
| 		const ushort *unicode,
 | |
| 		int &sor,
 | |
| 		int &eor,
 | |
| 		BidiControl &control,
 | |
| 		String::TextBlocks::const_iterator i) {
 | |
| 	String::TextBlocks::const_iterator e = _t->_blocks.cend(), n = i + 1;
 | |
| 
 | |
| 	QChar::Direction dir = control.basicDirection();
 | |
| 	int level = sor > 0 ? analysis[sor - 1].bidiLevel : control.level;
 | |
| 	while (sor <= _paragraphLength) {
 | |
| 		while (i != _paragraphStartBlock && (*i)->position() > _paragraphStart + sor) {
 | |
| 			n = i;
 | |
| 			--i;
 | |
| 		}
 | |
| 		while (n != e && (*n)->position() <= _paragraphStart + sor) {
 | |
| 			i = n;
 | |
| 			++n;
 | |
| 		}
 | |
| 
 | |
| 		TextBlockType _itype = (*i)->type();
 | |
| 		if (eor == _paragraphLength)
 | |
| 			dir = control.basicDirection();
 | |
| 		else if (_itype == TextBlockType::Emoji
 | |
| 			|| _itype == TextBlockType::CustomEmoji)
 | |
| 			dir = QChar::DirCS;
 | |
| 		else if (_itype == TextBlockType::Skip)
 | |
| 			dir = QChar::DirCS;
 | |
| 		else
 | |
| 			dir = QChar::direction(unicode[sor]);
 | |
| 		// Keep skipping DirBN as if it doesn't exist
 | |
| 		if (dir != QChar::DirBN)
 | |
| 			break;
 | |
| 		analysis[sor++].bidiLevel = level;
 | |
| 	}
 | |
| 
 | |
| 	eor = sor;
 | |
| 
 | |
| 	return dir;
 | |
| }
 | |
| 
 | |
| // creates the next QScript items.
 | |
| bool Renderer::eBidiItemize(QScriptAnalysis *analysis, BidiControl &control) {
 | |
| 	bool rightToLeft = (control.basicDirection() == 1);
 | |
| 	bool hasBidi = rightToLeft;
 | |
| 
 | |
| 	int sor = 0;
 | |
| 	int eor = -1;
 | |
| 
 | |
| 	const ushort *unicode = reinterpret_cast<const ushort*>(_t->_text.unicode()) + _paragraphStart;
 | |
| 	int current = 0;
 | |
| 
 | |
| 	QChar::Direction dir = rightToLeft ? QChar::DirR : QChar::DirL;
 | |
| 	BidiStatus status;
 | |
| 
 | |
| 	String::TextBlocks::const_iterator i = _paragraphStartBlock, e = _t->_blocks.cend(), n = i + 1;
 | |
| 
 | |
| 	QChar::Direction sdir;
 | |
| 	TextBlockType _stype = (*_paragraphStartBlock)->type();
 | |
| 	if (_stype == TextBlockType::Emoji || _stype == TextBlockType::CustomEmoji)
 | |
| 		sdir = QChar::DirCS;
 | |
| 	else if (_stype == TextBlockType::Skip)
 | |
| 		sdir = QChar::DirCS;
 | |
| 	else
 | |
| 		sdir = QChar::direction(*unicode);
 | |
| 	if (sdir != QChar::DirL && sdir != QChar::DirR && sdir != QChar::DirEN && sdir != QChar::DirAN)
 | |
| 		sdir = QChar::DirON;
 | |
| 	else
 | |
| 		dir = QChar::DirON;
 | |
| 
 | |
| 	status.eor = sdir;
 | |
| 	status.lastStrong = rightToLeft ? QChar::DirR : QChar::DirL;
 | |
| 	status.last = status.lastStrong;
 | |
| 	status.dir = sdir;
 | |
| 
 | |
| 	while (current <= _paragraphLength) {
 | |
| 		while (n != e && (*n)->position() <= _paragraphStart + current) {
 | |
| 			i = n;
 | |
| 			++n;
 | |
| 		}
 | |
| 
 | |
| 		QChar::Direction dirCurrent;
 | |
| 		TextBlockType _itype = (*i)->type();
 | |
| 		if (current == (int)_paragraphLength)
 | |
| 			dirCurrent = control.basicDirection();
 | |
| 		else if (_itype == TextBlockType::Emoji
 | |
| 			|| _itype == TextBlockType::CustomEmoji)
 | |
| 			dirCurrent = QChar::DirCS;
 | |
| 		else if (_itype == TextBlockType::Skip)
 | |
| 			dirCurrent = QChar::DirCS;
 | |
| 		else
 | |
| 			dirCurrent = QChar::direction(unicode[current]);
 | |
| 
 | |
| 		switch (dirCurrent) {
 | |
| 
 | |
| 			// embedding and overrides (X1-X9 in the BiDi specs)
 | |
| 		case QChar::DirRLE:
 | |
| 		case QChar::DirRLO:
 | |
| 		case QChar::DirLRE:
 | |
| 		case QChar::DirLRO:
 | |
| 		{
 | |
| 			bool rtl = (dirCurrent == QChar::DirRLE || dirCurrent == QChar::DirRLO);
 | |
| 			hasBidi |= rtl;
 | |
| 			bool override = (dirCurrent == QChar::DirLRO || dirCurrent == QChar::DirRLO);
 | |
| 
 | |
| 			unsigned int level = control.level + 1;
 | |
| 			if ((level % 2 != 0) == rtl) ++level;
 | |
| 			if (level < _MaxBidiLevel) {
 | |
| 				eor = current - 1;
 | |
| 				eAppendItems(analysis, sor, eor, control, dir);
 | |
| 				eor = current;
 | |
| 				control.embed(rtl, override);
 | |
| 				QChar::Direction edir = (rtl ? QChar::DirR : QChar::DirL);
 | |
| 				dir = status.eor = edir;
 | |
| 				status.lastStrong = edir;
 | |
| 			}
 | |
| 			break;
 | |
| 		}
 | |
| 		case QChar::DirPDF:
 | |
| 		{
 | |
| 			if (control.canPop()) {
 | |
| 				if (dir != control.direction()) {
 | |
| 					eor = current - 1;
 | |
| 					eAppendItems(analysis, sor, eor, control, dir);
 | |
| 					dir = control.direction();
 | |
| 				}
 | |
| 				eor = current;
 | |
| 				eAppendItems(analysis, sor, eor, control, dir);
 | |
| 				control.pdf();
 | |
| 				dir = QChar::DirON; status.eor = QChar::DirON;
 | |
| 				status.last = control.direction();
 | |
| 				if (control.override)
 | |
| 					dir = control.direction();
 | |
| 				else
 | |
| 					dir = QChar::DirON;
 | |
| 				status.lastStrong = control.direction();
 | |
| 			}
 | |
| 			break;
 | |
| 		}
 | |
| 
 | |
| 		// strong types
 | |
| 		case QChar::DirL:
 | |
| 			if (dir == QChar::DirON)
 | |
| 				dir = QChar::DirL;
 | |
| 			switch (status.last)
 | |
| 			{
 | |
| 			case QChar::DirL:
 | |
| 				eor = current; status.eor = QChar::DirL; break;
 | |
| 			case QChar::DirR:
 | |
| 			case QChar::DirAL:
 | |
| 			case QChar::DirEN:
 | |
| 			case QChar::DirAN:
 | |
| 				if (eor >= 0) {
 | |
| 					eAppendItems(analysis, sor, eor, control, dir);
 | |
| 					status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i);
 | |
| 				} else {
 | |
| 					eor = current; status.eor = dir;
 | |
| 				}
 | |
| 				break;
 | |
| 			case QChar::DirES:
 | |
| 			case QChar::DirET:
 | |
| 			case QChar::DirCS:
 | |
| 			case QChar::DirBN:
 | |
| 			case QChar::DirB:
 | |
| 			case QChar::DirS:
 | |
| 			case QChar::DirWS:
 | |
| 			case QChar::DirON:
 | |
| 				if (dir != QChar::DirL) {
 | |
| 					//last stuff takes embedding dir
 | |
| 					if (control.direction() == QChar::DirR) {
 | |
| 						if (status.eor != QChar::DirR) {
 | |
| 							// AN or EN
 | |
| 							eAppendItems(analysis, sor, eor, control, dir);
 | |
| 							status.eor = QChar::DirON;
 | |
| 							dir = QChar::DirR;
 | |
| 						}
 | |
| 						eor = current - 1;
 | |
| 						eAppendItems(analysis, sor, eor, control, dir);
 | |
| 						status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i);
 | |
| 					} else {
 | |
| 						if (status.eor != QChar::DirL) {
 | |
| 							eAppendItems(analysis, sor, eor, control, dir);
 | |
| 							status.eor = QChar::DirON;
 | |
| 							dir = QChar::DirL;
 | |
| 						} else {
 | |
| 							eor = current; status.eor = QChar::DirL; break;
 | |
| 						}
 | |
| 					}
 | |
| 				} else {
 | |
| 					eor = current; status.eor = QChar::DirL;
 | |
| 				}
 | |
| 			default:
 | |
| 				break;
 | |
| 			}
 | |
| 			status.lastStrong = QChar::DirL;
 | |
| 			break;
 | |
| 		case QChar::DirAL:
 | |
| 		case QChar::DirR:
 | |
| 			hasBidi = true;
 | |
| 			if (dir == QChar::DirON) dir = QChar::DirR;
 | |
| 			switch (status.last)
 | |
| 			{
 | |
| 			case QChar::DirL:
 | |
| 			case QChar::DirEN:
 | |
| 			case QChar::DirAN:
 | |
| 				if (eor >= 0)
 | |
| 					eAppendItems(analysis, sor, eor, control, dir);
 | |
| 				// fall through
 | |
| 			case QChar::DirR:
 | |
| 			case QChar::DirAL:
 | |
| 				dir = QChar::DirR; eor = current; status.eor = QChar::DirR; break;
 | |
| 			case QChar::DirES:
 | |
| 			case QChar::DirET:
 | |
| 			case QChar::DirCS:
 | |
| 			case QChar::DirBN:
 | |
| 			case QChar::DirB:
 | |
| 			case QChar::DirS:
 | |
| 			case QChar::DirWS:
 | |
| 			case QChar::DirON:
 | |
| 				if (status.eor != QChar::DirR && status.eor != QChar::DirAL) {
 | |
| 					//last stuff takes embedding dir
 | |
| 					if (control.direction() == QChar::DirR
 | |
| 						|| status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) {
 | |
| 						eAppendItems(analysis, sor, eor, control, dir);
 | |
| 						dir = QChar::DirR; status.eor = QChar::DirON;
 | |
| 						eor = current;
 | |
| 					} else {
 | |
| 						eor = current - 1;
 | |
| 						eAppendItems(analysis, sor, eor, control, dir);
 | |
| 						dir = QChar::DirR; status.eor = QChar::DirON;
 | |
| 					}
 | |
| 				} else {
 | |
| 					eor = current; status.eor = QChar::DirR;
 | |
| 				}
 | |
| 			default:
 | |
| 				break;
 | |
| 			}
 | |
| 			status.lastStrong = dirCurrent;
 | |
| 			break;
 | |
| 
 | |
| 			// weak types:
 | |
| 
 | |
| 		case QChar::DirNSM:
 | |
| 			if (eor == current - 1)
 | |
| 				eor = current;
 | |
| 			break;
 | |
| 		case QChar::DirEN:
 | |
| 			// if last strong was AL change EN to AN
 | |
| 			if (status.lastStrong != QChar::DirAL) {
 | |
| 				if (dir == QChar::DirON) {
 | |
| 					if (status.lastStrong == QChar::DirL)
 | |
| 						dir = QChar::DirL;
 | |
| 					else
 | |
| 						dir = QChar::DirEN;
 | |
| 				}
 | |
| 				switch (status.last)
 | |
| 				{
 | |
| 				case QChar::DirET:
 | |
| 					if (status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) {
 | |
| 						eAppendItems(analysis, sor, eor, control, dir);
 | |
| 						status.eor = QChar::DirON;
 | |
| 						dir = QChar::DirAN;
 | |
| 					}
 | |
| 					[[fallthrough]];
 | |
| 				case QChar::DirEN:
 | |
| 				case QChar::DirL:
 | |
| 					eor = current;
 | |
| 					status.eor = dirCurrent;
 | |
| 					break;
 | |
| 				case QChar::DirR:
 | |
| 				case QChar::DirAL:
 | |
| 				case QChar::DirAN:
 | |
| 					if (eor >= 0)
 | |
| 						eAppendItems(analysis, sor, eor, control, dir);
 | |
| 					else
 | |
| 						eor = current;
 | |
| 					status.eor = QChar::DirEN;
 | |
| 					dir = QChar::DirAN;
 | |
| 					break;
 | |
| 				case QChar::DirES:
 | |
| 				case QChar::DirCS:
 | |
| 					if (status.eor == QChar::DirEN || dir == QChar::DirAN) {
 | |
| 						eor = current; break;
 | |
| 					}
 | |
| 					[[fallthrough]];
 | |
| 				case QChar::DirBN:
 | |
| 				case QChar::DirB:
 | |
| 				case QChar::DirS:
 | |
| 				case QChar::DirWS:
 | |
| 				case QChar::DirON:
 | |
| 					if (status.eor == QChar::DirR) {
 | |
| 						// neutrals go to R
 | |
| 						eor = current - 1;
 | |
| 						eAppendItems(analysis, sor, eor, control, dir);
 | |
| 						status.eor = QChar::DirEN;
 | |
| 						dir = QChar::DirAN;
 | |
| 					} else if (status.eor == QChar::DirL ||
 | |
| 						(status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) {
 | |
| 						eor = current; status.eor = dirCurrent;
 | |
| 					} else {
 | |
| 						// numbers on both sides, neutrals get right to left direction
 | |
| 						if (dir != QChar::DirL) {
 | |
| 							eAppendItems(analysis, sor, eor, control, dir);
 | |
| 							status.eor = QChar::DirON;
 | |
| 							eor = current - 1;
 | |
| 							dir = QChar::DirR;
 | |
| 							eAppendItems(analysis, sor, eor, control, dir);
 | |
| 							status.eor = QChar::DirON;
 | |
| 							dir = QChar::DirAN;
 | |
| 						} else {
 | |
| 							eor = current; status.eor = dirCurrent;
 | |
| 						}
 | |
| 					}
 | |
| 					[[fallthrough]];
 | |
| 				default:
 | |
| 					break;
 | |
| 				}
 | |
| 				break;
 | |
| 			}
 | |
| 			[[fallthrough]];
 | |
| 		case QChar::DirAN:
 | |
| 			hasBidi = true;
 | |
| 			dirCurrent = QChar::DirAN;
 | |
| 			if (dir == QChar::DirON) dir = QChar::DirAN;
 | |
| 			switch (status.last)
 | |
| 			{
 | |
| 			case QChar::DirL:
 | |
| 			case QChar::DirAN:
 | |
| 				eor = current; status.eor = QChar::DirAN;
 | |
| 				break;
 | |
| 			case QChar::DirR:
 | |
| 			case QChar::DirAL:
 | |
| 			case QChar::DirEN:
 | |
| 				if (eor >= 0) {
 | |
| 					eAppendItems(analysis, sor, eor, control, dir);
 | |
| 				} else {
 | |
| 					eor = current;
 | |
| 				}
 | |
| 				dir = QChar::DirAN; status.eor = QChar::DirAN;
 | |
| 				break;
 | |
| 			case QChar::DirCS:
 | |
| 				if (status.eor == QChar::DirAN) {
 | |
| 					eor = current; break;
 | |
| 				}
 | |
| 				[[fallthrough]];
 | |
| 			case QChar::DirES:
 | |
| 			case QChar::DirET:
 | |
| 			case QChar::DirBN:
 | |
| 			case QChar::DirB:
 | |
| 			case QChar::DirS:
 | |
| 			case QChar::DirWS:
 | |
| 			case QChar::DirON:
 | |
| 				if (status.eor == QChar::DirR) {
 | |
| 					// neutrals go to R
 | |
| 					eor = current - 1;
 | |
| 					eAppendItems(analysis, sor, eor, control, dir);
 | |
| 					status.eor = QChar::DirAN;
 | |
| 					dir = QChar::DirAN;
 | |
| 				} else if (status.eor == QChar::DirL ||
 | |
| 					(status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) {
 | |
| 					eor = current; status.eor = dirCurrent;
 | |
| 				} else {
 | |
| 					// numbers on both sides, neutrals get right to left direction
 | |
| 					if (dir != QChar::DirL) {
 | |
| 						eAppendItems(analysis, sor, eor, control, dir);
 | |
| 						status.eor = QChar::DirON;
 | |
| 						eor = current - 1;
 | |
| 						dir = QChar::DirR;
 | |
| 						eAppendItems(analysis, sor, eor, control, dir);
 | |
| 						status.eor = QChar::DirAN;
 | |
| 						dir = QChar::DirAN;
 | |
| 					} else {
 | |
| 						eor = current; status.eor = dirCurrent;
 | |
| 					}
 | |
| 				}
 | |
| 				[[fallthrough]];
 | |
| 			default:
 | |
| 				break;
 | |
| 			}
 | |
| 			break;
 | |
| 		case QChar::DirES:
 | |
| 		case QChar::DirCS:
 | |
| 			break;
 | |
| 		case QChar::DirET:
 | |
| 			if (status.last == QChar::DirEN) {
 | |
| 				dirCurrent = QChar::DirEN;
 | |
| 				eor = current; status.eor = dirCurrent;
 | |
| 			}
 | |
| 			break;
 | |
| 
 | |
| 			// boundary neutrals should be ignored
 | |
| 		case QChar::DirBN:
 | |
| 			break;
 | |
| 			// neutrals
 | |
| 		case QChar::DirB:
 | |
| 			// ### what do we do with newline and paragraph separators that come to here?
 | |
| 			break;
 | |
| 		case QChar::DirS:
 | |
| 			// ### implement rule L1
 | |
| 			break;
 | |
| 		case QChar::DirWS:
 | |
| 		case QChar::DirON:
 | |
| 			break;
 | |
| 		default:
 | |
| 			break;
 | |
| 		}
 | |
| 
 | |
| 		if (current >= (int)_paragraphLength) break;
 | |
| 
 | |
| 		// set status.last as needed.
 | |
| 		switch (dirCurrent) {
 | |
| 		case QChar::DirET:
 | |
| 		case QChar::DirES:
 | |
| 		case QChar::DirCS:
 | |
| 		case QChar::DirS:
 | |
| 		case QChar::DirWS:
 | |
| 		case QChar::DirON:
 | |
| 			switch (status.last)
 | |
| 			{
 | |
| 			case QChar::DirL:
 | |
| 			case QChar::DirR:
 | |
| 			case QChar::DirAL:
 | |
| 			case QChar::DirEN:
 | |
| 			case QChar::DirAN:
 | |
| 				status.last = dirCurrent;
 | |
| 				break;
 | |
| 			default:
 | |
| 				status.last = QChar::DirON;
 | |
| 			}
 | |
| 			break;
 | |
| 		case QChar::DirNSM:
 | |
| 		case QChar::DirBN:
 | |
| 			// ignore these
 | |
| 			break;
 | |
| 		case QChar::DirLRO:
 | |
| 		case QChar::DirLRE:
 | |
| 			status.last = QChar::DirL;
 | |
| 			break;
 | |
| 		case QChar::DirRLO:
 | |
| 		case QChar::DirRLE:
 | |
| 			status.last = QChar::DirR;
 | |
| 			break;
 | |
| 		case QChar::DirEN:
 | |
| 			if (status.last == QChar::DirL) {
 | |
| 				status.last = QChar::DirL;
 | |
| 				break;
 | |
| 			}
 | |
| 			[[fallthrough]];
 | |
| 		default:
 | |
| 			status.last = dirCurrent;
 | |
| 		}
 | |
| 
 | |
| 		++current;
 | |
| 	}
 | |
| 
 | |
| 	eor = current - 1; // remove dummy char
 | |
| 
 | |
| 	if (sor <= eor)
 | |
| 		eAppendItems(analysis, sor, eor, control, dir);
 | |
| 
 | |
| 	return hasBidi;
 | |
| }
 | |
| 
 | |
| void Renderer::applyBlockProperties(const AbstractBlock *block) {
 | |
| 	eSetFont(block);
 | |
| 	if (_p) {
 | |
| 		const auto flags = block->flags();
 | |
| 		const auto isMono = IsMono(flags);
 | |
| 		_background = {};
 | |
| 		if ((flags & TextBlockFlag::Spoiler) && _spoiler) {
 | |
| 			_background.spoiler = true;
 | |
| 		}
 | |
| 		if (isMono
 | |
| 			&& block->linkIndex()
 | |
| 			&& (!_background.spoiler || _spoiler->revealed)) {
 | |
| 			const auto pressed = ClickHandler::showAsPressed(_t->_extended
 | |
| 				? _t->_extended->links[block->linkIndex() - 1]
 | |
| 				: nullptr);
 | |
| 			_background.selectActiveBlock = pressed;
 | |
| 		}
 | |
| 
 | |
| 		if (const auto color = block->colorIndex()) {
 | |
| 			if (color == 1) {
 | |
| 				if (_quote && _quote->blockquote && _quoteBlockquoteCache) {
 | |
| 					_quoteLinkPenOverride = QPen(_quoteBlockquoteCache->outlines[0]);
 | |
| 					_currentPen = &_quoteLinkPenOverride;
 | |
| 					_currentPenSelected = &_quoteLinkPenOverride;
 | |
| 				} else {
 | |
| 					_currentPen = &_palette->linkFg->p;
 | |
| 					_currentPenSelected = &_palette->selectLinkFg->p;
 | |
| 				}
 | |
| 			} else if (color - 1 <= _colors.size()) {
 | |
| 				_currentPen = _colors[color - 2].pen;
 | |
| 				_currentPenSelected = _colors[color - 2].penSelected;
 | |
| 			} else {
 | |
| 				_currentPen = &_originalPen;
 | |
| 				_currentPenSelected = &_originalPenSelected;
 | |
| 			}
 | |
| 		} else if (isMono) {
 | |
| 			_currentPen = &_palette->monoFg->p;
 | |
| 			_currentPenSelected = &_palette->selectMonoFg->p;
 | |
| 		} else if (block->linkIndex()) {
 | |
| 			if (_quote && _quote->blockquote && _quoteBlockquoteCache) {
 | |
| 				_quoteLinkPenOverride = QPen(_quoteBlockquoteCache->outlines[0]);
 | |
| 				_currentPen = &_quoteLinkPenOverride;
 | |
| 				_currentPenSelected = &_quoteLinkPenOverride;
 | |
| 			} else {
 | |
| 				_currentPen = &_palette->linkFg->p;
 | |
| 				_currentPenSelected = &_palette->selectLinkFg->p;
 | |
| 			}
 | |
| 		} else {
 | |
| 			_currentPen = &_originalPen;
 | |
| 			_currentPenSelected = &_originalPenSelected;
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| ClickHandlerPtr Renderer::lookupLink(const AbstractBlock *block) const {
 | |
| 	const auto spoilerLink = (_spoiler
 | |
| 		&& !_spoiler->revealed
 | |
| 		&& (block->flags() & TextBlockFlag::Spoiler))
 | |
| 		? _spoiler->link
 | |
| 		: ClickHandlerPtr();
 | |
| 	return (spoilerLink || !block->linkIndex() || !_t->_extended)
 | |
| 		? spoilerLink
 | |
| 		: _t->_extended->links[block->linkIndex() - 1];
 | |
| }
 | |
| 
 | |
| } // namespace Ui::Text
 | 
