1 // This file is part of Desktop App Toolkit,
2 // a set of libraries for developing nice desktop applications.
3 //
4 // For license and copyright information please follow this link:
5 // https://github.com/desktop-app/legal/blob/master/LEGAL
6 //
7 #include "ui/text/text.h"
8 
9 #include "ui/basic_click_handlers.h"
10 #include "ui/text/text_block.h"
11 #include "ui/text/text_isolated_emoji.h"
12 #include "ui/emoji_config.h"
13 #include "ui/integration.h"
14 #include "base/platform/base_platform_info.h"
15 #include "base/qt_adapters.h"
16 
17 #include <private/qfontengine_p.h>
18 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
19 #include <private/qharfbuzz_p.h>
20 #endif // Qt < 6.0.0
21 
22 namespace Ui {
23 namespace Text {
24 namespace {
25 
26 constexpr auto kStringLinkIndexShift = uint16(0x8000);
27 constexpr auto kMaxDiacAfterSymbol = 2;
28 
StringDirection(const QString & str,int from,int to)29 Qt::LayoutDirection StringDirection(
30 		const QString &str,
31 		int from,
32 		int to) {
33 	auto p = reinterpret_cast<const ushort*>(str.unicode()) + from;
34 	const auto end = p + (to - from);
35 	while (p < end) {
36 		uint ucs4 = *p;
37 		if (QChar::isHighSurrogate(ucs4) && p < end - 1) {
38 			ushort low = p[1];
39 			if (QChar::isLowSurrogate(low)) {
40 				ucs4 = QChar::surrogateToUcs4(ucs4, low);
41 				++p;
42 			}
43 		}
44 		switch (QChar::direction(ucs4)) {
45 		case QChar::DirL:
46 			return Qt::LeftToRight;
47 		case QChar::DirR:
48 		case QChar::DirAL:
49 			return Qt::RightToLeft;
50 		default:
51 			break;
52 		}
53 		++p;
54 	}
55 	return Qt::LayoutDirectionAuto;
56 }
57 
PrepareRichFromPlain(const QString & text,const TextParseOptions & options)58 TextWithEntities PrepareRichFromPlain(
59 		const QString &text,
60 		const TextParseOptions &options) {
61 	auto result = TextWithEntities{ text };
62 	if (options.flags & TextParseLinks) {
63 		TextUtilities::ParseEntities(
64 			result,
65 			options.flags,
66 			(options.flags & TextParseRichText));
67 	}
68 	return result;
69 }
70 
PrepareRichFromRich(const TextWithEntities & text,const TextParseOptions & options)71 TextWithEntities PrepareRichFromRich(
72 		const TextWithEntities &text,
73 		const TextParseOptions &options) {
74 	auto result = text;
75 	const auto &preparsed = text.entities;
76 	if ((options.flags & TextParseLinks) && !preparsed.isEmpty()) {
77 		bool parseMentions = (options.flags & TextParseMentions);
78 		bool parseHashtags = (options.flags & TextParseHashtags);
79 		bool parseBotCommands = (options.flags & TextParseBotCommands);
80 		bool parseMarkdown = (options.flags & TextParseMarkdown);
81 		if (!parseMentions || !parseHashtags || !parseBotCommands || !parseMarkdown) {
82 			int32 i = 0, l = preparsed.size();
83 			result.entities.clear();
84 			result.entities.reserve(l);
85 			for (; i < l; ++i) {
86 				auto type = preparsed.at(i).type();
87 				if (((type == EntityType::Mention || type == EntityType::MentionName) && !parseMentions) ||
88 					(type == EntityType::Hashtag && !parseHashtags) ||
89 					(type == EntityType::Cashtag && !parseHashtags) ||
90 					(type == EntityType::BotCommand && !parseBotCommands) || // #TODO entities
91 					(!parseMarkdown && (type == EntityType::Bold
92 						|| type == EntityType::Semibold
93 						|| type == EntityType::Italic
94 						|| type == EntityType::Underline
95 						|| type == EntityType::StrikeOut
96 						|| type == EntityType::Code
97 						|| type == EntityType::Pre))) {
98 					continue;
99 				}
100 				result.entities.push_back(preparsed.at(i));
101 			}
102 		}
103 	}
104 	return result;
105 }
106 
ComputeStopAfter(const TextParseOptions & options,const style::TextStyle & st)107 QFixed ComputeStopAfter(
108 		const TextParseOptions &options,
109 		const style::TextStyle &st) {
110 	return (options.maxw > 0 && options.maxh > 0)
111 		? ((options.maxh / st.font->height) + 1) * options.maxw
112 		: QFIXED_MAX;
113 }
114 
115 // Open Sans tilde fix.
ComputeCheckTilde(const style::TextStyle & st)116 bool ComputeCheckTilde(const style::TextStyle &st) {
117 	const auto &font = st.font;
118 	return (font->size() * style::DevicePixelRatio() == 13)
119 		&& (font->flags() == 0)
120 		&& (font->f.family() == qstr("DAOpenSansRegular"));
121 }
122 
IsParagraphSeparator(QChar ch)123 bool IsParagraphSeparator(QChar ch) {
124 	switch (ch.unicode()) {
125 	case QChar::LineFeed:
126 		return true;
127 	default:
128 		break;
129 	}
130 	return false;
131 }
132 
IsBad(QChar ch)133 bool IsBad(QChar ch) {
134 	return (ch == 0)
135 		|| (ch >= 8232 && ch < 8237)
136 		|| (ch >= 65024 && ch < 65040 && ch != 65039)
137 		|| (ch >= 127 && ch < 160 && ch != 156)
138 
139 		// qt harfbuzz crash see https://github.com/telegramdesktop/tdesktop/issues/4551
140 		|| (Platform::IsMac() && ch == 6158);
141 }
142 
143 } // namespace
144 } // namespace Text
145 } // namespace Ui
146 
textcmdSkipBlock(ushort w,ushort h)147 QString textcmdSkipBlock(ushort w, ushort h) {
148 	static QString cmd(5, TextCommand);
149 	cmd[1] = QChar(TextCommandSkipBlock);
150 	cmd[2] = QChar(w);
151 	cmd[3] = QChar(h);
152 	return cmd;
153 }
154 
textcmdStartLink(ushort lnkIndex)155 QString textcmdStartLink(ushort lnkIndex) {
156 	static QString cmd(4, TextCommand);
157 	cmd[1] = QChar(TextCommandLinkIndex);
158 	cmd[2] = QChar(lnkIndex);
159 	return cmd;
160 }
161 
textcmdStartLink(const QString & url)162 QString textcmdStartLink(const QString &url) {
163 	if (url.size() >= 4096) return QString();
164 
165 	QString result;
166 	result.reserve(url.size() + 4);
167 	return result.append(TextCommand).append(QChar(TextCommandLinkText)).append(QChar(int(url.size()))).append(url).append(TextCommand);
168 }
169 
textcmdStopLink()170 QString textcmdStopLink() {
171 	return textcmdStartLink(0);
172 }
173 
textcmdLink(ushort lnkIndex,const QString & text)174 QString textcmdLink(ushort lnkIndex, const QString &text) {
175 	QString result;
176 	result.reserve(4 + text.size() + 4);
177 	return result.append(textcmdStartLink(lnkIndex)).append(text).append(textcmdStopLink());
178 }
179 
textcmdLink(const QString & url,const QString & text)180 QString textcmdLink(const QString &url, const QString &text) {
181 	QString result;
182 	result.reserve(4 + url.size() + text.size() + 4);
183 	return result.append(textcmdStartLink(url)).append(text).append(textcmdStopLink());
184 }
185 
textcmdStartSemibold()186 QString textcmdStartSemibold() {
187 	QString result;
188 	result.reserve(3);
189 	return result.append(TextCommand).append(QChar(TextCommandSemibold)).append(TextCommand);
190 }
191 
textcmdStopSemibold()192 QString textcmdStopSemibold() {
193 	QString result;
194 	result.reserve(3);
195 	return result.append(TextCommand).append(QChar(TextCommandNoSemibold)).append(TextCommand);
196 }
197 
textSkipCommand(const QChar * from,const QChar * end,bool canLink)198 const QChar *textSkipCommand(const QChar *from, const QChar *end, bool canLink) {
199 	const QChar *result = from + 1;
200 	if (*from != TextCommand || result >= end) return from;
201 
202 	ushort cmd = result->unicode();
203 	++result;
204 	if (result >= end) return from;
205 
206 	switch (cmd) {
207 	case TextCommandBold:
208 	case TextCommandNoBold:
209 	case TextCommandSemibold:
210 	case TextCommandNoSemibold:
211 	case TextCommandItalic:
212 	case TextCommandNoItalic:
213 	case TextCommandUnderline:
214 	case TextCommandNoUnderline:
215 		break;
216 
217 	case TextCommandLinkIndex:
218 		if (result->unicode() > 0x7FFF) return from;
219 		++result;
220 		break;
221 
222 	case TextCommandLinkText: {
223 		ushort len = result->unicode();
224 		if (len >= 4096 || !canLink) return from;
225 		result += len + 1;
226 	} break;
227 
228 	case TextCommandSkipBlock:
229 		result += 2;
230 		break;
231 
232 	case TextCommandLangTag:
233 		result += 1;
234 		break;
235 	}
236 	return (result < end && *result == TextCommand) ? (result + 1) : from;
237 }
238 
239 const TextParseOptions _defaultOptions = {
240 	TextParseLinks | TextParseMultiline, // flags
241 	0, // maxw
242 	0, // maxh
243 	Qt::LayoutDirectionAuto, // dir
244 };
245 
246 const TextParseOptions _textPlainOptions = {
247 	TextParseMultiline, // flags
248 	0, // maxw
249 	0, // maxh
250 	Qt::LayoutDirectionAuto, // dir
251 };
252 
253 namespace Ui {
254 namespace Text {
255 
256 class Parser {
257 public:
258 	Parser(
259 		not_null<String*> string,
260 		const QString &text,
261 		const TextParseOptions &options,
262 		const std::any &context);
263 	Parser(
264 		not_null<String*> string,
265 		const TextWithEntities &textWithEntities,
266 		const TextParseOptions &options,
267 		const std::any &context);
268 
269 private:
270 	struct ReadyToken {
271 	};
272 
273 	class StartedEntity {
274 	public:
275 		explicit StartedEntity(TextBlockFlags flags);
276 		explicit StartedEntity(uint16 lnkIndex);
277 
278 		std::optional<TextBlockFlags> flags() const;
279 		std::optional<uint16> lnkIndex() const;
280 
281 	private:
282 		int _value = 0;
283 
284 	};
285 
286 	Parser(
287 		not_null<String*> string,
288 		TextWithEntities &&source,
289 		const TextParseOptions &options,
290 		const std::any &context,
291 		ReadyToken);
292 
293 	void trimSourceRange();
294 	void blockCreated();
295 	void createBlock(int32 skipBack = 0);
296 	void createSkipBlock(int32 w, int32 h);
297 	void createNewlineBlock();
298 	bool checkCommand();
299 
300 	// Returns true if at least one entity was parsed in the current position.
301 	bool checkEntities();
302 	bool readSkipBlockCommand();
303 	bool readCommand();
304 	void parseCurrentChar();
305 	void parseEmojiFromCurrent();
306 	void checkForElidedSkipBlock();
307 	void finalize(const TextParseOptions &options);
308 
309 	void finishEntities();
310 	void skipPassedEntities();
311 	void skipBadEntities();
312 
313 	bool isInvalidEntity(const EntityInText &entity) const;
314 	bool isLinkEntity(const EntityInText &entity) const;
315 
316 	void parse(const TextParseOptions &options);
317 	void computeLinkText(
318 		const QString &linkData,
319 		QString *outLinkText,
320 		EntityLinkShown *outShown);
321 
322 	const not_null<String*> _t;
323 	const TextWithEntities _source;
324 	const std::any &_context;
325 	const QChar * const _start = nullptr;
326 	const QChar *_end = nullptr; // mutable, because we trim by decrementing.
327 	const QChar *_ptr = nullptr;
328 	const EntitiesInText::const_iterator _entitiesEnd;
329 	EntitiesInText::const_iterator _waitingEntity;
330 	const bool _rich = false;
331 	const bool _multiline = false;
332 
333 	const QFixed _stopAfterWidth; // summary width of all added words
334 	const bool _checkTilde = false; // do we need a special text block for tilde symbol
335 
336 	std::vector<EntityLinkData> _links;
337 	base::flat_map<
338 		const QChar*,
339 		std::vector<StartedEntity>> _startedEntities;
340 
341 	uint16 _maxLnkIndex = 0;
342 
343 	// current state
344 	int32 _flags = 0;
345 	uint16 _lnkIndex = 0;
346 	EmojiPtr _emoji = nullptr; // current emoji, if current word is an emoji, or zero
347 	int32 _blockStart = 0; // offset in result, from which current parsed block is started
348 	int32 _diacs = 0; // diac chars skipped without good char
349 	QFixed _sumWidth;
350 	bool _sumFinished = false;
351 	bool _newlineAwaited = false;
352 
353 	// current char data
354 	QChar _ch; // current char (low surrogate, if current char is surrogate pair)
355 	int32 _emojiLookback = 0; // how far behind the current ptr to look for current emoji
356 	bool _lastSkipped = false; // did we skip current char
357 
358 };
359 
StartedEntity(TextBlockFlags flags)360 Parser::StartedEntity::StartedEntity(TextBlockFlags flags) : _value(flags) {
361 	Expects(_value >= 0 && _value < int(kStringLinkIndexShift));
362 }
363 
StartedEntity(uint16 lnkIndex)364 Parser::StartedEntity::StartedEntity(uint16 lnkIndex) : _value(lnkIndex) {
365 	Expects(_value >= kStringLinkIndexShift);
366 }
367 
flags() const368 std::optional<TextBlockFlags> Parser::StartedEntity::flags() const {
369 	if (_value < int(kStringLinkIndexShift)) {
370 		return TextBlockFlags(_value);
371 	}
372 	return std::nullopt;
373 }
374 
lnkIndex() const375 std::optional<uint16> Parser::StartedEntity::lnkIndex() const {
376 	if (_value >= int(kStringLinkIndexShift)) {
377 		return uint16(_value);
378 	}
379 	return std::nullopt;
380 }
381 
Parser(not_null<String * > string,const QString & text,const TextParseOptions & options,const std::any & context)382 Parser::Parser(
383 	not_null<String*> string,
384 	const QString &text,
385 	const TextParseOptions &options,
386 	const std::any &context)
387 : Parser(
388 	string,
389 	PrepareRichFromPlain(text, options),
390 	options,
391 	context,
392 	ReadyToken()) {
393 }
394 
Parser(not_null<String * > string,const TextWithEntities & textWithEntities,const TextParseOptions & options,const std::any & context)395 Parser::Parser(
396 	not_null<String*> string,
397 	const TextWithEntities &textWithEntities,
398 	const TextParseOptions &options,
399 	const std::any &context)
400 : Parser(
401 	string,
402 	PrepareRichFromRich(textWithEntities, options),
403 	options,
404 	context,
405 	ReadyToken()) {
406 }
407 
Parser(not_null<String * > string,TextWithEntities && source,const TextParseOptions & options,const std::any & context,ReadyToken)408 Parser::Parser(
409 	not_null<String*> string,
410 	TextWithEntities &&source,
411 	const TextParseOptions &options,
412 	const std::any &context,
413 	ReadyToken)
414 : _t(string)
415 , _source(std::move(source))
416 , _context(context)
417 , _start(_source.text.constData())
418 , _end(_start + _source.text.size())
419 , _ptr(_start)
420 , _entitiesEnd(_source.entities.end())
421 , _waitingEntity(_source.entities.begin())
422 , _rich(options.flags & TextParseRichText)
423 , _multiline(options.flags & TextParseMultiline)
424 , _stopAfterWidth(ComputeStopAfter(options, *_t->_st))
425 , _checkTilde(ComputeCheckTilde(*_t->_st)) {
426 	parse(options);
427 }
428 
blockCreated()429 void Parser::blockCreated() {
430 	_sumWidth += _t->_blocks.back()->f_width();
431 	if (_sumWidth.floor().toInt() > _stopAfterWidth) {
432 		_sumFinished = true;
433 	}
434 }
435 
createBlock(int32 skipBack)436 void Parser::createBlock(int32 skipBack) {
437 	if (_lnkIndex < kStringLinkIndexShift && _lnkIndex > _maxLnkIndex) {
438 		_maxLnkIndex = _lnkIndex;
439 	}
440 
441 	int32 len = int32(_t->_text.size()) + skipBack - _blockStart;
442 	if (len > 0) {
443 		bool newline = !_emoji && (len == 1 && _t->_text.at(_blockStart) == QChar::LineFeed);
444 		if (_newlineAwaited) {
445 			_newlineAwaited = false;
446 			if (!newline) {
447 				_t->_text.insert(_blockStart, QChar::LineFeed);
448 				createBlock(skipBack - len);
449 			}
450 		}
451 		_lastSkipped = false;
452 		if (_emoji) {
453 			_t->_blocks.push_back(Block::Emoji(_t->_st->font, _t->_text, _blockStart, len, _flags, _lnkIndex, _emoji));
454 			_emoji = nullptr;
455 			_lastSkipped = true;
456 		} else if (newline) {
457 			_t->_blocks.push_back(Block::Newline(_t->_st->font, _t->_text, _blockStart, len, _flags, _lnkIndex));
458 		} else {
459 			_t->_blocks.push_back(Block::Text(_t->_st->font, _t->_text, _t->_minResizeWidth, _blockStart, len, _flags, _lnkIndex));
460 		}
461 		_blockStart += len;
462 		blockCreated();
463 	}
464 }
465 
createSkipBlock(int32 w,int32 h)466 void Parser::createSkipBlock(int32 w, int32 h) {
467 	createBlock();
468 	_t->_text.push_back('_');
469 	_t->_blocks.push_back(Block::Skip(_t->_st->font, _t->_text, _blockStart++, w, h, _lnkIndex));
470 	blockCreated();
471 }
472 
createNewlineBlock()473 void Parser::createNewlineBlock() {
474 	createBlock();
475 	_t->_text.push_back(QChar::LineFeed);
476 	createBlock();
477 }
478 
checkCommand()479 bool Parser::checkCommand() {
480 	bool result = false;
481 	for (QChar c = ((_ptr < _end) ? *_ptr : 0); c == TextCommand; c = ((_ptr < _end) ? *_ptr : 0)) {
482 		if (!readCommand()) {
483 			break;
484 		}
485 		result = true;
486 	}
487 	return result;
488 }
489 
finishEntities()490 void Parser::finishEntities() {
491 	while (!_startedEntities.empty()
492 		&& (_ptr >= _startedEntities.begin()->first || _ptr >= _end)) {
493 		auto list = std::move(_startedEntities.begin()->second);
494 		_startedEntities.erase(_startedEntities.begin());
495 
496 		while (!list.empty()) {
497 			if (const auto flags = list.back().flags()) {
498 				if (_flags & (*flags)) {
499 					createBlock();
500 					_flags &= ~(*flags);
501 					if (((*flags) & TextBlockFPre)
502 						&& !_t->_blocks.empty()
503 						&& _t->_blocks.back()->type() != TextBlockTNewline) {
504 						_newlineAwaited = true;
505 					}
506 				}
507 			} else if (const auto lnkIndex = list.back().lnkIndex()) {
508 				if (_lnkIndex == *lnkIndex) {
509 					createBlock();
510 					_lnkIndex = 0;
511 				}
512 			}
513 			list.pop_back();
514 		}
515 	}
516 }
517 
518 // Returns true if at least one entity was parsed in the current position.
checkEntities()519 bool Parser::checkEntities() {
520 	finishEntities();
521 	skipPassedEntities();
522 	if (_waitingEntity == _entitiesEnd
523 		|| _ptr < _start + _waitingEntity->offset()) {
524 		return false;
525 	}
526 
527 	auto flags = TextBlockFlags();
528 	auto link = EntityLinkData();
529 	const auto entityType = _waitingEntity->type();
530 	const auto entityLength = _waitingEntity->length();
531 	const auto entityBegin = _start + _waitingEntity->offset();
532 	const auto entityEnd = entityBegin + entityLength;
533 	const auto pushSimpleUrl = [&](EntityType type) {
534 		link.type = type;
535 		link.data = QString(entityBegin, entityLength);
536 		if (type == EntityType::Url) {
537 			computeLinkText(link.data, &link.text, &link.shown);
538 		} else {
539 			link.text = link.data;
540 		}
541 	};
542 	const auto pushComplexUrl = [&] {
543 		link.type = entityType;
544 		link.data = _waitingEntity->data();
545 		link.text = QString(entityBegin, entityLength);
546 	};
547 	if (entityType == EntityType::Bold) {
548 		flags = TextBlockFBold;
549 	} else if (entityType == EntityType::Semibold) {
550 		flags = TextBlockFSemibold;
551 	} else if (entityType == EntityType::Italic) {
552 		flags = TextBlockFItalic;
553 	} else if (entityType == EntityType::Underline) {
554 		flags = TextBlockFUnderline;
555 	} else if (entityType == EntityType::StrikeOut) {
556 		flags = TextBlockFStrikeOut;
557 	} else if (entityType == EntityType::Code) { // #TODO entities
558 		flags = TextBlockFCode;
559 	} else if (entityType == EntityType::Pre) {
560 		flags = TextBlockFPre;
561 		createBlock();
562 		if (!_t->_blocks.empty() && _t->_blocks.back()->type() != TextBlockTNewline) {
563 			createNewlineBlock();
564 		}
565 	} else if (entityType == EntityType::Url
566 		|| entityType == EntityType::Email
567 		|| entityType == EntityType::Mention
568 		|| entityType == EntityType::Hashtag
569 		|| entityType == EntityType::Cashtag
570 		|| entityType == EntityType::BotCommand) {
571 		pushSimpleUrl(entityType);
572 	} else if (entityType == EntityType::CustomUrl) {
573 		const auto url = _waitingEntity->data();
574 		const auto text = QString(entityBegin, entityLength);
575 		if (url == text) {
576 			pushSimpleUrl(EntityType::Url);
577 		} else {
578 			pushComplexUrl();
579 		}
580 	} else if (entityType == EntityType::MentionName) {
581 		pushComplexUrl();
582 	}
583 
584 	if (link.type != EntityType::Invalid) {
585 		createBlock();
586 
587 		_links.push_back(link);
588 		_lnkIndex = kStringLinkIndexShift + _links.size();
589 
590 		_startedEntities[entityEnd].emplace_back(_lnkIndex);
591 	} else if (flags) {
592 		if (!(_flags & flags)) {
593 			createBlock();
594 			_flags |= flags;
595 			_startedEntities[entityEnd].emplace_back(flags);
596 		}
597 	}
598 
599 	++_waitingEntity;
600 	skipBadEntities();
601 	return true;
602 }
603 
skipPassedEntities()604 void Parser::skipPassedEntities() {
605 	while (_waitingEntity != _entitiesEnd
606 		&& _start + _waitingEntity->offset() + _waitingEntity->length() <= _ptr) {
607 		++_waitingEntity;
608 	}
609 }
610 
skipBadEntities()611 void Parser::skipBadEntities() {
612 	if (_links.size() >= 0x7FFF) {
613 		while (_waitingEntity != _entitiesEnd
614 			&& (isLinkEntity(*_waitingEntity)
615 				|| isInvalidEntity(*_waitingEntity))) {
616 			++_waitingEntity;
617 		}
618 	} else {
619 		while (_waitingEntity != _entitiesEnd && isInvalidEntity(*_waitingEntity)) {
620 			++_waitingEntity;
621 		}
622 	}
623 }
624 
readSkipBlockCommand()625 bool Parser::readSkipBlockCommand() {
626 	const QChar *afterCmd = textSkipCommand(_ptr, _end, _links.size() < 0x7FFF);
627 	if (afterCmd == _ptr) {
628 		return false;
629 	}
630 
631 	ushort cmd = (++_ptr)->unicode();
632 	++_ptr;
633 
634 	switch (cmd) {
635 	case TextCommandSkipBlock:
636 		createSkipBlock(_ptr->unicode(), (_ptr + 1)->unicode());
637 	break;
638 	}
639 
640 	_ptr = afterCmd;
641 	return true;
642 }
643 
readCommand()644 bool Parser::readCommand() {
645 	const QChar *afterCmd = textSkipCommand(_ptr, _end, _links.size() < 0x7FFF);
646 	if (afterCmd == _ptr) {
647 		return false;
648 	}
649 
650 	ushort cmd = (++_ptr)->unicode();
651 	++_ptr;
652 
653 	switch (cmd) {
654 	case TextCommandBold:
655 		if (!(_flags & TextBlockFBold)) {
656 			createBlock();
657 			_flags |= TextBlockFBold;
658 		}
659 	break;
660 
661 	case TextCommandNoBold:
662 		if (_flags & TextBlockFBold) {
663 			createBlock();
664 			_flags &= ~TextBlockFBold;
665 		}
666 	break;
667 
668 	case TextCommandSemibold:
669 	if (!(_flags & TextBlockFSemibold)) {
670 		createBlock();
671 		_flags |= TextBlockFSemibold;
672 	}
673 	break;
674 
675 	case TextCommandNoSemibold:
676 	if (_flags & TextBlockFSemibold) {
677 		createBlock();
678 		_flags &= ~TextBlockFSemibold;
679 	}
680 	break;
681 
682 	case TextCommandItalic:
683 		if (!(_flags & TextBlockFItalic)) {
684 			createBlock();
685 			_flags |= TextBlockFItalic;
686 		}
687 	break;
688 
689 	case TextCommandNoItalic:
690 		if (_flags & TextBlockFItalic) {
691 			createBlock();
692 			_flags &= ~TextBlockFItalic;
693 		}
694 	break;
695 
696 	case TextCommandUnderline:
697 		if (!(_flags & TextBlockFUnderline)) {
698 			createBlock();
699 			_flags |= TextBlockFUnderline;
700 		}
701 	break;
702 
703 	case TextCommandNoUnderline:
704 		if (_flags & TextBlockFUnderline) {
705 			createBlock();
706 			_flags &= ~TextBlockFUnderline;
707 		}
708 	break;
709 
710 	case TextCommandStrikeOut:
711 		if (!(_flags & TextBlockFStrikeOut)) {
712 			createBlock();
713 			_flags |= TextBlockFStrikeOut;
714 		}
715 		break;
716 
717 	case TextCommandNoStrikeOut:
718 		if (_flags & TextBlockFStrikeOut) {
719 			createBlock();
720 			_flags &= ~TextBlockFStrikeOut;
721 		}
722 		break;
723 
724 	case TextCommandLinkIndex:
725 		if (_ptr->unicode() != _lnkIndex) {
726 			createBlock();
727 			_lnkIndex = _ptr->unicode();
728 		}
729 	break;
730 
731 	case TextCommandLinkText: {
732 		createBlock();
733 		int32 len = _ptr->unicode();
734 		_links.push_back(EntityLinkData{
735 			.data = QString(++_ptr, len),
736 			.type = EntityType::CustomUrl
737 		});
738 		_lnkIndex = kStringLinkIndexShift + _links.size();
739 	} break;
740 
741 	case TextCommandSkipBlock:
742 		createSkipBlock(_ptr->unicode(), (_ptr + 1)->unicode());
743 	break;
744 	}
745 
746 	_ptr = afterCmd;
747 	return true;
748 }
749 
parseCurrentChar()750 void Parser::parseCurrentChar() {
751 	_ch = ((_ptr < _end) ? *_ptr : 0);
752 	_emojiLookback = 0;
753 	const auto isNewLine = _multiline && IsNewline(_ch);
754 	const auto isSpace = IsSpace(_ch);
755 	const auto isDiac = IsDiac(_ch);
756 	const auto isTilde = _checkTilde && (_ch == '~');
757 	const auto skip = [&] {
758 		if (IsBad(_ch) || _ch.isLowSurrogate()) {
759 			return true;
760 		} else if (_ch == 0xFE0F && Platform::IsMac()) {
761 			// Some sequences like 0x0E53 0xFE0F crash OS X harfbuzz text processing :(
762 			return true;
763 		} else if (isDiac) {
764 			if (_lastSkipped || _emoji || ++_diacs > kMaxDiacAfterSymbol) {
765 				return true;
766 			}
767 		} else if (_ch.isHighSurrogate()) {
768 			if (_ptr + 1 >= _end || !(_ptr + 1)->isLowSurrogate()) {
769 				return true;
770 			}
771 			const auto ucs4 = QChar::surrogateToUcs4(_ch, *(_ptr + 1));
772 			if (ucs4 >= 0xE0000) {
773 				// Unicode tags are skipped.
774 				// Only place they work is in some flag emoji,
775 				// but in that case they were already parsed as emoji before.
776 				//
777 				// For unknown reason in some unknown cases strings with such
778 				// symbols lead to crashes on some Linux distributions, see
779 				// https://github.com/telegramdesktop/tdesktop/issues/7005
780 				//
781 				// At least one crashing text was starting that way:
782 				//
783 				// 0xd83d 0xdcda 0xdb40 0xdc69 0xdb40 0xdc64 0xdb40 0xdc6a
784 				// 0xdb40 0xdc77 0xdb40 0xdc7f 0x32 ... simple text here ...
785 				//
786 				// or in codepoints:
787 				//
788 				// 0x1f4da 0xe0069 0xe0064 0xe006a 0xe0077 0xe007f 0x32 ...
789 				return true;
790 			}
791 		}
792 		return false;
793 	}();
794 
795 	if (_ch.isHighSurrogate() && !skip) {
796 		_t->_text.push_back(_ch);
797 		++_ptr;
798 		_ch = *_ptr;
799 		_emojiLookback = 1;
800 	}
801 
802 	_lastSkipped = skip;
803 	if (skip) {
804 		_ch = 0;
805 	} else {
806 		if (isTilde) { // tilde fix in OpenSans
807 			if (!(_flags & TextBlockFTilde)) {
808 				createBlock(-_emojiLookback);
809 				_flags |= TextBlockFTilde;
810 			}
811 		} else {
812 			if (_flags & TextBlockFTilde) {
813 				createBlock(-_emojiLookback);
814 				_flags &= ~TextBlockFTilde;
815 			}
816 		}
817 		if (isNewLine) {
818 			createNewlineBlock();
819 		} else if (isSpace) {
820 			_t->_text.push_back(QChar::Space);
821 		} else {
822 			if (_emoji) {
823 				createBlock(-_emojiLookback);
824 			}
825 			_t->_text.push_back(_ch);
826 		}
827 		if (!isDiac) _diacs = 0;
828 	}
829 }
830 
parseEmojiFromCurrent()831 void Parser::parseEmojiFromCurrent() {
832 	int len = 0;
833 	auto e = Emoji::Find(_ptr - _emojiLookback, _end, &len);
834 	if (!e) return;
835 
836 	for (int l = len - _emojiLookback - 1; l > 0; --l) {
837 		_t->_text.push_back(*++_ptr);
838 	}
839 	if (e->hasPostfix()) {
840 		Assert(!_t->_text.isEmpty());
841 		const auto last = _t->_text[_t->_text.size() - 1];
842 		if (last.unicode() != Emoji::kPostfix) {
843 			_t->_text.push_back(QChar(Emoji::kPostfix));
844 			++len;
845 		}
846 	}
847 
848 	createBlock(-len);
849 	_emoji = e;
850 }
851 
isInvalidEntity(const EntityInText & entity) const852 bool Parser::isInvalidEntity(const EntityInText &entity) const {
853 	const auto length = entity.length();
854 	return (_start + entity.offset() + length > _end) || (length <= 0);
855 }
856 
isLinkEntity(const EntityInText & entity) const857 bool Parser::isLinkEntity(const EntityInText &entity) const {
858 	const auto type = entity.type();
859 	const auto urls = {
860 		EntityType::Url,
861 		EntityType::CustomUrl,
862 		EntityType::Email,
863 		EntityType::Hashtag,
864 		EntityType::Cashtag,
865 		EntityType::Mention,
866 		EntityType::MentionName,
867 		EntityType::BotCommand
868 	};
869 	return ranges::find(urls, type) != std::end(urls);
870 }
871 
parse(const TextParseOptions & options)872 void Parser::parse(const TextParseOptions &options) {
873 	skipBadEntities();
874 	trimSourceRange();
875 
876 	_t->_text.resize(0);
877 	_t->_text.reserve(_end - _ptr);
878 
879 	for (; _ptr <= _end; ++_ptr) {
880 		while (checkEntities() || (_rich && checkCommand())) {
881 		}
882 		parseCurrentChar();
883 		parseEmojiFromCurrent();
884 
885 		if (_sumFinished || _t->_text.size() >= 0x8000) {
886 			break; // 32k max
887 		}
888 	}
889 	createBlock();
890 	checkForElidedSkipBlock();
891 	finalize(options);
892 }
893 
trimSourceRange()894 void Parser::trimSourceRange() {
895 	const auto firstMonospaceOffset = EntityInText::FirstMonospaceOffset(
896 		_source.entities,
897 		_end - _start);
898 
899 	while (_ptr != _end && IsTrimmed(*_ptr, _rich) && _ptr != _start + firstMonospaceOffset) {
900 		++_ptr;
901 	}
902 	while (_ptr != _end && IsTrimmed(*(_end - 1), _rich)) {
903 		--_end;
904 	}
905 }
906 
checkForElidedSkipBlock()907 void Parser::checkForElidedSkipBlock() {
908 	if (!_sumFinished || !_rich) {
909 		return;
910 	}
911 	// We could've skipped the final skip block command.
912 	for (; _ptr < _end; ++_ptr) {
913 		if (*_ptr == TextCommand && readSkipBlockCommand()) {
914 			break;
915 		}
916 	}
917 }
918 
finalize(const TextParseOptions & options)919 void Parser::finalize(const TextParseOptions &options) {
920 	_t->_links.resize(_maxLnkIndex);
921 	for (auto &block : _t->_blocks) {
922 		const auto shiftedIndex = block->lnkIndex();
923 		if (shiftedIndex <= kStringLinkIndexShift) {
924 			continue;
925 		}
926 		const auto realIndex = (shiftedIndex - kStringLinkIndexShift);
927 		const auto index = _maxLnkIndex + realIndex;
928 		block->setLnkIndex(index);
929 		if (_t->_links.size() >= index) {
930 			continue;
931 		}
932 
933 		_t->_links.resize(index);
934 		const auto handler = Integration::Instance().createLinkHandler(
935 			_links[realIndex - 1],
936 			_context);
937 		if (handler) {
938 			_t->setLink(index, handler);
939 		}
940 	}
941 	_t->_links.squeeze();
942 	_t->_blocks.squeeze();
943 	_t->_text.squeeze();
944 }
945 
computeLinkText(const QString & linkData,QString * outLinkText,EntityLinkShown * outShown)946 void Parser::computeLinkText(
947 		const QString &linkData,
948 		QString *outLinkText,
949 		EntityLinkShown *outShown) {
950 	auto url = QUrl(linkData);
951 	auto good = QUrl(url.isValid()
952 		? url.toEncoded()
953 		: QByteArray());
954 	auto readable = good.isValid()
955 		? good.toDisplayString()
956 		: linkData;
957 	*outLinkText = _t->_st->font->elided(readable, st::linkCropLimit);
958 	*outShown = (*outLinkText == readable)
959 		? EntityLinkShown::Full
960 		: EntityLinkShown::Partial;
961 }
962 
963 namespace {
964 
965 // COPIED FROM qtextengine.cpp AND MODIFIED
966 
967 struct BidiStatus {
BidiStatusUi::Text::__anon237552240511::BidiStatus968 	BidiStatus() {
969 		eor = QChar::DirON;
970 		lastStrong = QChar::DirON;
971 		last = QChar:: DirON;
972 		dir = QChar::DirON;
973 	}
974 	QChar::Direction eor;
975 	QChar::Direction lastStrong;
976 	QChar::Direction last;
977 	QChar::Direction dir;
978 };
979 
980 enum { _MaxBidiLevel = 61 };
981 enum { _MaxItemLength = 4096 };
982 
983 struct BidiControl {
BidiControlUi::Text::__anon237552240511::BidiControl984 	inline BidiControl(bool rtl)
985 		: base(rtl ? 1 : 0), level(rtl ? 1 : 0) {}
986 
embedUi::Text::__anon237552240511::BidiControl987 	inline void embed(bool rtl, bool o = false) {
988 		unsigned int toAdd = 1;
989 		if((level%2 != 0) == rtl ) {
990 			++toAdd;
991 		}
992 		if (level + toAdd <= _MaxBidiLevel) {
993 			ctx[cCtx].level = level;
994 			ctx[cCtx].override = override;
995 			cCtx++;
996 			override = o;
997 			level += toAdd;
998 		}
999 	}
canPopUi::Text::__anon237552240511::BidiControl1000 	inline bool canPop() const { return cCtx != 0; }
pdfUi::Text::__anon237552240511::BidiControl1001 	inline void pdf() {
1002 		Q_ASSERT(cCtx);
1003 		--cCtx;
1004 		level = ctx[cCtx].level;
1005 		override = ctx[cCtx].override;
1006 	}
1007 
basicDirectionUi::Text::__anon237552240511::BidiControl1008 	inline QChar::Direction basicDirection() const {
1009 		return (base ? QChar::DirR : QChar:: DirL);
1010 	}
baseLevelUi::Text::__anon237552240511::BidiControl1011 	inline unsigned int baseLevel() const {
1012 		return base;
1013 	}
directionUi::Text::__anon237552240511::BidiControl1014 	inline QChar::Direction direction() const {
1015 		return ((level%2) ? QChar::DirR : QChar:: DirL);
1016 	}
1017 
1018 	struct {
1019 		unsigned int level = 0;
1020 		bool override = false;
1021 	} ctx[_MaxBidiLevel];
1022 	unsigned int cCtx = 0;
1023 	const unsigned int base;
1024 	unsigned int level;
1025 	bool override = false;
1026 };
1027 
eAppendItems(QScriptAnalysis * analysis,int & start,int & stop,const BidiControl & control,QChar::Direction dir)1028 static void eAppendItems(QScriptAnalysis *analysis, int &start, int &stop, const BidiControl &control, QChar::Direction dir) {
1029 	if (start > stop)
1030 		return;
1031 
1032 	int level = control.level;
1033 
1034 	if(dir != QChar::DirON && !control.override) {
1035 		// add level of run (cases I1 & I2)
1036 		if(level % 2) {
1037 			if(dir == QChar::DirL || dir == QChar::DirAN || dir == QChar::DirEN)
1038 				level++;
1039 		} else {
1040 			if(dir == QChar::DirR)
1041 				level++;
1042 			else if(dir == QChar::DirAN || dir == QChar::DirEN)
1043 				level += 2;
1044 		}
1045 	}
1046 
1047 	QScriptAnalysis *s = analysis + start;
1048 	const QScriptAnalysis *e = analysis + stop;
1049 	while (s <= e) {
1050 		s->bidiLevel = level;
1051 		++s;
1052 	}
1053 	++stop;
1054 	start = stop;
1055 }
1056 
countBlockHeight(const AbstractBlock * b,const style::TextStyle * st)1057 inline int32 countBlockHeight(const AbstractBlock *b, const style::TextStyle *st) {
1058 	return (b->type() == TextBlockTSkip) ? static_cast<const SkipBlock*>(b)->height() : (st->lineHeight > st->font->height) ? st->lineHeight : st->font->height;
1059 }
1060 
1061 } // namespace
1062 
1063 class Renderer {
1064 public:
Renderer(Painter * p,const String * t)1065 	Renderer(Painter *p, const String *t)
1066 	: _p(p)
1067 	, _t(t)
1068 	, _originalPen(p ? p->pen() : QPen()) {
1069 	}
1070 
~Renderer()1071 	~Renderer() {
1072 		restoreAfterElided();
1073 		if (_p) {
1074 			_p->setPen(_originalPen);
1075 		}
1076 	}
1077 
draw(int32 left,int32 top,int32 w,style::align align,int32 yFrom,int32 yTo,TextSelection selection={ 0, 0 },bool fullWidthSelection=true)1078 	void draw(int32 left, int32 top, int32 w, style::align align, int32 yFrom, int32 yTo, TextSelection selection = { 0, 0 }, bool fullWidthSelection = true) {
1079 		if (_t->isEmpty()) return;
1080 
1081 		_blocksSize = _t->_blocks.size();
1082 		if (_p) {
1083 			_p->setFont(_t->_st->font);
1084 			_textPalette = &_p->textPalette();
1085 			_originalPenSelected = (_textPalette->selectFg->c.alphaF() == 0) ? _originalPen : _textPalette->selectFg->p;
1086 		}
1087 
1088 		_x = left;
1089 		_y = top;
1090 		_yFrom = yFrom + top;
1091 		_yTo = (yTo < 0) ? -1 : (yTo + top);
1092 		_selection = selection;
1093 		_fullWidthSelection = fullWidthSelection;
1094 		_wLeft = _w = w;
1095 		if (_elideLast) {
1096 			_yToElide = _yTo;
1097 			if (_elideRemoveFromEnd > 0 && !_t->_blocks.empty()) {
1098 				int firstBlockHeight = countBlockHeight(_t->_blocks.front().get(), _t->_st);
1099 				if (_y + firstBlockHeight >= _yToElide) {
1100 					_wLeft -= _elideRemoveFromEnd;
1101 				}
1102 			}
1103 		}
1104 		_str = _t->_text.unicode();
1105 
1106 		if (_p) {
1107 			auto clip = _p->hasClipping() ? _p->clipBoundingRect() : QRect();
1108 			if (clip.width() > 0 || clip.height() > 0) {
1109 				if (_yFrom < clip.y()) _yFrom = clip.y();
1110 				if (_yTo < 0 || _yTo > clip.y() + clip.height()) _yTo = clip.y() + clip.height();
1111 			}
1112 		}
1113 
1114 		_align = align;
1115 
1116 		_parDirection = _t->_startDir;
1117 		if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = style::LayoutDirection();
1118 		if ((*_t->_blocks.cbegin())->type() != TextBlockTNewline) {
1119 			initNextParagraph(_t->_blocks.cbegin());
1120 		}
1121 
1122 		_lineStart = 0;
1123 		_lineStartBlock = 0;
1124 
1125 		_lineHeight = 0;
1126 		_fontHeight = _t->_st->font->height;
1127 		auto last_rBearing = QFixed(0);
1128 		_last_rPadding = QFixed(0);
1129 
1130 		auto blockIndex = 0;
1131 		bool longWordLine = true;
1132 		auto e = _t->_blocks.cend();
1133 		for (auto i = _t->_blocks.cbegin(); i != e; ++i, ++blockIndex) {
1134 			auto b = i->get();
1135 			auto _btype = b->type();
1136 			auto blockHeight = countBlockHeight(b, _t->_st);
1137 
1138 			if (_btype == TextBlockTNewline) {
1139 				if (!_lineHeight) _lineHeight = blockHeight;
1140 				if (!drawLine((*i)->from(), i, e)) {
1141 					return;
1142 				}
1143 
1144 				_y += _lineHeight;
1145 				_lineHeight = 0;
1146 				_lineStart = _t->countBlockEnd(i, e);
1147 				_lineStartBlock = blockIndex + 1;
1148 
1149 				last_rBearing = b->f_rbearing();
1150 				_last_rPadding = b->f_rpadding();
1151 				_wLeft = _w - (b->f_width() - last_rBearing);
1152 				if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) {
1153 					_wLeft -= _elideRemoveFromEnd;
1154 				}
1155 
1156 				_parDirection = static_cast<const NewlineBlock*>(b)->nextDirection();
1157 				if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = style::LayoutDirection();
1158 				initNextParagraph(i + 1);
1159 
1160 				longWordLine = true;
1161 				continue;
1162 			}
1163 
1164 			auto b__f_rbearing = b->f_rbearing();
1165 			auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + b->f_width() - b__f_rbearing);
1166 			if (newWidthLeft >= 0) {
1167 				last_rBearing = b__f_rbearing;
1168 				_last_rPadding = b->f_rpadding();
1169 				_wLeft = newWidthLeft;
1170 
1171 				_lineHeight = qMax(_lineHeight, blockHeight);
1172 
1173 				longWordLine = false;
1174 				continue;
1175 			}
1176 
1177 			if (_btype == TextBlockTText) {
1178 				auto t = static_cast<const TextBlock*>(b);
1179 				if (t->_words.isEmpty()) { // no words in this block, spaces only => layout this block in the same line
1180 					_last_rPadding += b->f_rpadding();
1181 
1182 					_lineHeight = qMax(_lineHeight, blockHeight);
1183 
1184 					longWordLine = false;
1185 					continue;
1186 				}
1187 
1188 				auto f_wLeft = _wLeft; // vars for saving state of the last word start
1189 				auto f_lineHeight = _lineHeight; // f points to the last word-start element of t->_words
1190 				for (auto j = t->_words.cbegin(), en = t->_words.cend(), f = j; j != en; ++j) {
1191 					auto wordEndsHere = (j->f_width() >= 0);
1192 					auto j_width = wordEndsHere ? j->f_width() : -j->f_width();
1193 
1194 					auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + j_width - j->f_rbearing());
1195 					if (newWidthLeft >= 0) {
1196 						last_rBearing = j->f_rbearing();
1197 						_last_rPadding = j->f_rpadding();
1198 						_wLeft = newWidthLeft;
1199 
1200 						_lineHeight = qMax(_lineHeight, blockHeight);
1201 
1202 						if (wordEndsHere) {
1203 							longWordLine = false;
1204 						}
1205 						if (wordEndsHere || longWordLine) {
1206 							f = j + 1;
1207 							f_wLeft = _wLeft;
1208 							f_lineHeight = _lineHeight;
1209 						}
1210 						continue;
1211 					}
1212 
1213 					auto elidedLineHeight = qMax(_lineHeight, blockHeight);
1214 					auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide);
1215 					if (elidedLine) {
1216 						_lineHeight = elidedLineHeight;
1217 					} else if (f != j && !_breakEverywhere) {
1218 						// word did not fit completely, so we roll back the state to the beginning of this long word
1219 						j = f;
1220 						_wLeft = f_wLeft;
1221 						_lineHeight = f_lineHeight;
1222 						j_width = (j->f_width() >= 0) ? j->f_width() : -j->f_width();
1223 					}
1224 					if (!drawLine(elidedLine ? ((j + 1 == en) ? _t->countBlockEnd(i, e) : (j + 1)->from()) : j->from(), i, e)) {
1225 						return;
1226 					}
1227 					_y += _lineHeight;
1228 					_lineHeight = qMax(0, blockHeight);
1229 					_lineStart = j->from();
1230 					_lineStartBlock = blockIndex;
1231 
1232 					last_rBearing = j->f_rbearing();
1233 					_last_rPadding = j->f_rpadding();
1234 					_wLeft = _w - (j_width - last_rBearing);
1235 					if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) {
1236 						_wLeft -= _elideRemoveFromEnd;
1237 					}
1238 
1239 					longWordLine = !wordEndsHere;
1240 					f = j + 1;
1241 					f_wLeft = _wLeft;
1242 					f_lineHeight = _lineHeight;
1243 				}
1244 				continue;
1245 			}
1246 
1247 			auto elidedLineHeight = qMax(_lineHeight, blockHeight);
1248 			auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide);
1249 			if (elidedLine) {
1250 				_lineHeight = elidedLineHeight;
1251 			}
1252 			if (!drawLine(elidedLine ? _t->countBlockEnd(i, e) : b->from(), i, e)) {
1253 				return;
1254 			}
1255 			_y += _lineHeight;
1256 			_lineHeight = qMax(0, blockHeight);
1257 			_lineStart = b->from();
1258 			_lineStartBlock = blockIndex;
1259 
1260 			last_rBearing = b__f_rbearing;
1261 			_last_rPadding = b->f_rpadding();
1262 			_wLeft = _w - (b->f_width() - last_rBearing);
1263 			if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) {
1264 				_wLeft -= _elideRemoveFromEnd;
1265 			}
1266 
1267 			longWordLine = true;
1268 			continue;
1269 		}
1270 		if (_lineStart < _t->_text.size()) {
1271 			if (!drawLine(_t->_text.size(), e, e)) return;
1272 		}
1273 		if (!_p && _lookupSymbol) {
1274 			_lookupResult.symbol = _t->_text.size();
1275 			_lookupResult.afterSymbol = false;
1276 		}
1277 	}
1278 
drawElided(int32 left,int32 top,int32 w,style::align align,int32 lines,int32 yFrom,int32 yTo,int32 removeFromEnd,bool breakEverywhere,TextSelection selection)1279 	void drawElided(int32 left, int32 top, int32 w, style::align align, int32 lines, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) {
1280 		if (lines <= 0 || _t->isNull()) return;
1281 
1282 		if (yTo < 0 || (lines - 1) * _t->_st->font->height < yTo) {
1283 			yTo = lines * _t->_st->font->height;
1284 			_elideLast = true;
1285 			_elideRemoveFromEnd = removeFromEnd;
1286 		}
1287 		_breakEverywhere = breakEverywhere;
1288 		draw(left, top, w, align, yFrom, yTo, selection);
1289 	}
1290 
getState(QPoint point,int w,StateRequest request)1291 	StateResult getState(QPoint point, int w, StateRequest request) {
1292 		if (!_t->isNull() && point.y() >= 0) {
1293 			_lookupRequest = request;
1294 			_lookupX = point.x();
1295 			_lookupY = point.y();
1296 
1297 			_breakEverywhere = (_lookupRequest.flags & StateRequest::Flag::BreakEverywhere);
1298 			_lookupSymbol = (_lookupRequest.flags & StateRequest::Flag::LookupSymbol);
1299 			_lookupLink = (_lookupRequest.flags & StateRequest::Flag::LookupLink);
1300 			if (_lookupSymbol || (_lookupX >= 0 && _lookupX < w)) {
1301 				draw(0, 0, w, _lookupRequest.align, _lookupY, _lookupY + 1);
1302 			}
1303 		}
1304 		return _lookupResult;
1305 	}
1306 
getStateElided(QPoint point,int w,StateRequestElided request)1307 	StateResult getStateElided(QPoint point, int w, StateRequestElided request) {
1308 		if (!_t->isNull() && point.y() >= 0 && request.lines > 0) {
1309 			_lookupRequest = request;
1310 			_lookupX = point.x();
1311 			_lookupY = point.y();
1312 
1313 			_breakEverywhere = (_lookupRequest.flags & StateRequest::Flag::BreakEverywhere);
1314 			_lookupSymbol = (_lookupRequest.flags & StateRequest::Flag::LookupSymbol);
1315 			_lookupLink = (_lookupRequest.flags & StateRequest::Flag::LookupLink);
1316 			if (_lookupSymbol || (_lookupX >= 0 && _lookupX < w)) {
1317 				int yTo = _lookupY + 1;
1318 				if (yTo < 0 || (request.lines - 1) * _t->_st->font->height < yTo) {
1319 					yTo = request.lines * _t->_st->font->height;
1320 					_elideLast = true;
1321 					_elideRemoveFromEnd = request.removeFromEnd;
1322 				}
1323 				draw(0, 0, w, _lookupRequest.align, _lookupY, _lookupY + 1);
1324 			}
1325 		}
1326 		return _lookupResult;
1327 	}
1328 
1329 private:
initNextParagraph(String::TextBlocks::const_iterator i)1330 	void initNextParagraph(String::TextBlocks::const_iterator i) {
1331 		_parStartBlock = i;
1332 		const auto e = _t->_blocks.cend();
1333 		if (i == e) {
1334 			_parStart = _t->_text.size();
1335 			_parLength = 0;
1336 		} else {
1337 			_parStart = (*i)->from();
1338 			for (; i != e; ++i) {
1339 				if ((*i)->type() == TextBlockTNewline) {
1340 					break;
1341 				}
1342 			}
1343 			_parLength = ((i == e) ? _t->_text.size() : (*i)->from()) - _parStart;
1344 		}
1345 		_parAnalysis.resize(0);
1346 	}
1347 
initParagraphBidi()1348 	void initParagraphBidi() {
1349 		if (!_parLength || !_parAnalysis.isEmpty()) return;
1350 
1351 		String::TextBlocks::const_iterator i = _parStartBlock, e = _t->_blocks.cend(), n = i + 1;
1352 
1353 		bool ignore = false;
1354 		bool rtl = (_parDirection == Qt::RightToLeft);
1355 		if (!ignore && !rtl) {
1356 			ignore = true;
1357 			const ushort *start = reinterpret_cast<const ushort*>(_str) + _parStart;
1358 			const ushort *curr = start;
1359 			const ushort *end = start + _parLength;
1360 			while (curr < end) {
1361 				while (n != e && (*n)->from() <= _parStart + (curr - start)) {
1362 					i = n;
1363 					++n;
1364 				}
1365 				if ((*i)->type() != TextBlockTEmoji && *curr >= 0x590) {
1366 					ignore = false;
1367 					break;
1368 				}
1369 				++curr;
1370 			}
1371 		}
1372 
1373 		_parAnalysis.resize(_parLength);
1374 		QScriptAnalysis *analysis = _parAnalysis.data();
1375 
1376 		BidiControl control(rtl);
1377 
1378 		_parHasBidi = false;
1379 		if (ignore) {
1380 			memset(analysis, 0, _parLength * sizeof(QScriptAnalysis));
1381 			if (rtl) {
1382 				for (int i = 0; i < _parLength; ++i)
1383 					analysis[i].bidiLevel = 1;
1384 				_parHasBidi = true;
1385 			}
1386 		} else {
1387 			_parHasBidi = eBidiItemize(analysis, control);
1388 		}
1389 	}
1390 
drawLine(uint16 _lineEnd,const String::TextBlocks::const_iterator & _endBlockIter,const String::TextBlocks::const_iterator & _end)1391 	bool drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterator &_endBlockIter, const String::TextBlocks::const_iterator &_end) {
1392 		_yDelta = (_lineHeight - _fontHeight) / 2;
1393 		if (_yTo >= 0 && (_y + _yDelta >= _yTo || _y >= _yTo)) return false;
1394 		if (_y + _yDelta + _fontHeight <= _yFrom) {
1395 			if (_lookupSymbol) {
1396 				_lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart;
1397 				_lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false;
1398 			}
1399 			return true;
1400 		}
1401 
1402 		// Trimming pending spaces, because they sometimes don't fit on the line.
1403 		// They also are not counted in the line width, they're in the right padding.
1404 		// Line width is a sum of block / word widths and paddings between them, without trailing one.
1405 		auto trimmedLineEnd = _lineEnd;
1406 		for (; trimmedLineEnd > _lineStart; --trimmedLineEnd) {
1407 			auto ch = _t->_text[trimmedLineEnd - 1];
1408 			if (ch != QChar::Space && ch != QChar::LineFeed) {
1409 				break;
1410 			}
1411 		}
1412 
1413 		auto _endBlock = (_endBlockIter == _end) ? nullptr : _endBlockIter->get();
1414 		auto elidedLine = _elideLast && (_y + _lineHeight >= _yToElide);
1415 		if (elidedLine) {
1416 			// If we decided to draw the last line elided only because of the skip block
1417 			// that did not fit on this line, we just draw the line till the very end.
1418 			// Skip block is ignored in the elided lines, instead "removeFromEnd" is used.
1419 			if (_endBlock && _endBlock->type() == TextBlockTSkip) {
1420 				_endBlock = nullptr;
1421 			}
1422 			if (!_endBlock) {
1423 				elidedLine = false;
1424 			}
1425 		}
1426 
1427 		auto blockIndex = _lineStartBlock;
1428 		auto currentBlock = _t->_blocks[blockIndex].get();
1429 		auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
1430 
1431 		const auto extendLeft = (currentBlock->from() < _lineStart)
1432 			? qMin(_lineStart - currentBlock->from(), 2)
1433 			: 0;
1434 		_localFrom = _lineStart - extendLeft;
1435 		const auto extendedLineEnd = (_endBlock && _endBlock->from() < trimmedLineEnd && !elidedLine)
1436 			? qMin(uint16(trimmedLineEnd + 2), _t->countBlockEnd(_endBlockIter, _end))
1437 			: trimmedLineEnd;
1438 
1439 		auto lineText = _t->_text.mid(_localFrom, extendedLineEnd - _localFrom);
1440 		auto lineStart = extendLeft;
1441 		auto lineLength = trimmedLineEnd - _lineStart;
1442 
1443 		if (elidedLine) {
1444 			initParagraphBidi();
1445 			prepareElidedLine(lineText, lineStart, lineLength, _endBlock);
1446 		}
1447 
1448 		auto x = _x;
1449 		if (_align & Qt::AlignHCenter) {
1450 			x += (_wLeft / 2).toInt();
1451 		} else if (((_align & Qt::AlignLeft) && _parDirection == Qt::RightToLeft) || ((_align & Qt::AlignRight) && _parDirection == Qt::LeftToRight)) {
1452 			x += _wLeft;
1453 		}
1454 
1455 		if (!_p) {
1456 			if (_lookupX < x) {
1457 				if (_lookupSymbol) {
1458 					if (_parDirection == Qt::RightToLeft) {
1459 						_lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart;
1460 						_lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false;
1461 //						_lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockTSkip)) ? true : false;
1462 					} else {
1463 						_lookupResult.symbol = _lineStart;
1464 						_lookupResult.afterSymbol = false;
1465 //						_lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineStart > 0)) ? true : false;
1466 					}
1467 				}
1468 				if (_lookupLink) {
1469 					_lookupResult.link = nullptr;
1470 				}
1471 				_lookupResult.uponSymbol = false;
1472 				return false;
1473 			} else if (_lookupX >= x + (_w - _wLeft)) {
1474 				if (_parDirection == Qt::RightToLeft) {
1475 					_lookupResult.symbol = _lineStart;
1476 					_lookupResult.afterSymbol = false;
1477 //					_lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineStart > 0)) ? true : false;
1478 				} else {
1479 					_lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart;
1480 					_lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false;
1481 //					_lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockTSkip)) ? true : false;
1482 				}
1483 				if (_lookupLink) {
1484 					_lookupResult.link = nullptr;
1485 				}
1486 				_lookupResult.uponSymbol = false;
1487 				return false;
1488 			}
1489 		}
1490 
1491 		if (_fullWidthSelection) {
1492 			const auto selectFromStart = (_selection.to > _lineStart)
1493 				&& (_lineStart > 0)
1494 				&& (_selection.from <= _lineStart);
1495 			const auto selectTillEnd = (_selection.to > trimmedLineEnd)
1496 				&& (trimmedLineEnd < _t->_text.size())
1497 				&& (_selection.from <= trimmedLineEnd)
1498 				&& (!_endBlock || _endBlock->type() != TextBlockTSkip);
1499 
1500 			if ((selectFromStart && _parDirection == Qt::LeftToRight)
1501 				|| (selectTillEnd && _parDirection == Qt::RightToLeft)) {
1502 				if (x > _x) {
1503 					fillSelectRange(_x, x);
1504 				}
1505 			}
1506 			if ((selectTillEnd && _parDirection == Qt::LeftToRight)
1507 				|| (selectFromStart && _parDirection == Qt::RightToLeft)) {
1508 				if (x < _x + _wLeft) {
1509 					fillSelectRange(x + _w - _wLeft, _x + _w);
1510 				}
1511 			}
1512 		}
1513 		if (trimmedLineEnd == _lineStart && !elidedLine) {
1514 			return true;
1515 		}
1516 
1517 		if (!elidedLine) initParagraphBidi(); // if was not inited
1518 
1519 		_f = _t->_st->font;
1520 		QStackTextEngine engine(lineText, _f->f);
1521 		engine.option.setTextDirection(_parDirection);
1522 		_e = &engine;
1523 
1524 		eItemize();
1525 
1526 		QScriptLine line;
1527 		line.from = lineStart;
1528 		line.length = lineLength;
1529 		eShapeLine(line);
1530 
1531 		int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1);
1532 		int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0;
1533 		if (!nItems) {
1534 			return true;
1535 		}
1536 
1537 		int skipIndex = -1;
1538 		QVarLengthArray<int> visualOrder(nItems);
1539 		QVarLengthArray<uchar> levels(nItems);
1540 		for (int i = 0; i < nItems; ++i) {
1541 			auto &si = engine.layoutData->items[firstItem + i];
1542 			while (nextBlock && nextBlock->from() <= _localFrom + si.position) {
1543 				currentBlock = nextBlock;
1544 				nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
1545 			}
1546 			auto _type = currentBlock->type();
1547 			if (_type == TextBlockTSkip) {
1548 				levels[i] = si.analysis.bidiLevel = 0;
1549 				skipIndex = i;
1550 			} else {
1551 				levels[i] = si.analysis.bidiLevel;
1552 			}
1553 			if (si.analysis.flags == QScriptAnalysis::Object) {
1554 				if (_type == TextBlockTEmoji || _type == TextBlockTSkip) {
1555 					si.width = currentBlock->f_width() + (nextBlock == _endBlock && (!nextBlock || nextBlock->from() >= trimmedLineEnd) ? 0 : currentBlock->f_rpadding());
1556 				}
1557 			}
1558 		}
1559 		QTextEngine::bidiReorder(nItems, levels.data(), visualOrder.data());
1560 		if (style::RightToLeft() && skipIndex == nItems - 1) {
1561 			for (int32 i = nItems; i > 1;) {
1562 				--i;
1563 				visualOrder[i] = visualOrder[i - 1];
1564 			}
1565 			visualOrder[0] = skipIndex;
1566 		}
1567 
1568 		blockIndex = _lineStartBlock;
1569 		currentBlock = _t->_blocks[blockIndex].get();
1570 		nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
1571 
1572 		int32 textY = _y + _yDelta + _t->_st->font->ascent, emojiY = (_t->_st->font->height - st::emojiSize) / 2;
1573 
1574 		applyBlockProperties(currentBlock);
1575 		for (int i = 0; i < nItems; ++i) {
1576 			int item = firstItem + visualOrder[i];
1577 			const QScriptItem &si = engine.layoutData->items.at(item);
1578 			bool rtl = (si.analysis.bidiLevel % 2);
1579 
1580 			while (blockIndex > _lineStartBlock + 1 && _t->_blocks[blockIndex - 1]->from() > _localFrom + si.position) {
1581 				nextBlock = currentBlock;
1582 				currentBlock = _t->_blocks[--blockIndex - 1].get();
1583 				applyBlockProperties(currentBlock);
1584 			}
1585 			while (nextBlock && nextBlock->from() <= _localFrom + si.position) {
1586 				currentBlock = nextBlock;
1587 				nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
1588 				applyBlockProperties(currentBlock);
1589 			}
1590 			if (si.analysis.flags >= QScriptAnalysis::TabOrObject) {
1591 				TextBlockType _type = currentBlock->type();
1592 				if (!_p && _lookupX >= x && _lookupX < x + si.width) { // _lookupRequest
1593 					if (_lookupLink) {
1594 						if (currentBlock->lnkIndex() && _lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
1595 							_lookupResult.link = _t->_links.at(currentBlock->lnkIndex() - 1);
1596 						}
1597 					}
1598 					if (_type != TextBlockTSkip) {
1599 						_lookupResult.uponSymbol = true;
1600 					}
1601 					if (_lookupSymbol) {
1602 						if (_type == TextBlockTSkip) {
1603 							if (_parDirection == Qt::RightToLeft) {
1604 								_lookupResult.symbol = _lineStart;
1605 								_lookupResult.afterSymbol = false;
1606 							} else {
1607 								_lookupResult.symbol = (trimmedLineEnd > _lineStart) ? (trimmedLineEnd - 1) : _lineStart;
1608 								_lookupResult.afterSymbol = (trimmedLineEnd > _lineStart) ? true : false;
1609 							}
1610 							return false;
1611 						}
1612 
1613 						// Emoji with spaces after symbol lookup
1614 						auto chFrom = _str + currentBlock->from();
1615 						auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from());
1616 						auto spacesWidth = (si.width - currentBlock->f_width());
1617 						auto spacesCount = 0;
1618 						while (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space) {
1619 							++spacesCount;
1620 							--chTo;
1621 						}
1622 						if (spacesCount > 0) { // Check if we're over a space.
1623 							if (rtl) {
1624 								if (_lookupX < x + spacesWidth) {
1625 									_lookupResult.symbol = (chTo - _str); // up to a space, included, rtl
1626 									_lookupResult.afterSymbol = (_lookupX < x + (spacesWidth / 2)) ? true : false;
1627 									return false;
1628 								}
1629 							} else if (_lookupX >= x + si.width - spacesWidth) {
1630 								_lookupResult.symbol = (chTo - _str); // up to a space, inclided, ltr
1631 								_lookupResult.afterSymbol = (_lookupX >= x + si.width - spacesWidth + (spacesWidth / 2)) ? true : false;
1632 								return false;
1633 							}
1634 						}
1635 						if (_lookupX < x + (rtl ? (si.width - currentBlock->f_width()) : 0) + (currentBlock->f_width() / 2)) {
1636 							_lookupResult.symbol = ((rtl && chTo > chFrom) ? (chTo - 1) : chFrom) - _str;
1637 							_lookupResult.afterSymbol = (rtl && chTo > chFrom) ? true : false;
1638 						} else {
1639 							_lookupResult.symbol = ((rtl || chTo <= chFrom) ? chFrom : (chTo - 1)) - _str;
1640 							_lookupResult.afterSymbol = (rtl || chTo <= chFrom) ? false : true;
1641 						}
1642 					}
1643 					return false;
1644 				} else if (_p && _type == TextBlockTEmoji) {
1645 					auto glyphX = x;
1646 					auto spacesWidth = (si.width - currentBlock->f_width());
1647 					if (rtl) {
1648 						glyphX += spacesWidth;
1649 					}
1650 					if (_localFrom + si.position < _selection.to) {
1651 						auto chFrom = _str + currentBlock->from();
1652 						auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from());
1653 						if (_localFrom + si.position >= _selection.from) { // could be without space
1654 							if (chTo == chFrom || (chTo - 1)->unicode() != QChar::Space || _selection.to >= (chTo - _str)) {
1655 								fillSelectRange(x, x + si.width);
1656 							} else { // or with space
1657 								fillSelectRange(glyphX, glyphX + currentBlock->f_width());
1658 							}
1659 						} else if (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space && (chTo - 1 - _str) >= _selection.from) {
1660 							if (rtl) { // rtl space only
1661 								fillSelectRange(x, glyphX);
1662 							} else { // ltr space only
1663 								fillSelectRange(x + currentBlock->f_width(), x + si.width);
1664 							}
1665 						}
1666 					}
1667 					Emoji::Draw(
1668 						*_p,
1669 						static_cast<const EmojiBlock*>(currentBlock)->_emoji,
1670 						Emoji::GetSizeNormal(),
1671 						(glyphX + st::emojiPadding).toInt(),
1672 						_y + _yDelta + emojiY);
1673 //				} else if (_p && currentBlock->type() == TextBlockSkip) { // debug
1674 //					_p->fillRect(QRect(x.toInt(), _y, currentBlock->width(), static_cast<SkipBlock*>(currentBlock)->height()), QColor(0, 0, 0, 32));
1675 				}
1676 				x += si.width;
1677 				continue;
1678 			}
1679 
1680 			unsigned short *logClusters = engine.logClusters(&si);
1681 			QGlyphLayout glyphs = engine.shapedGlyphs(&si);
1682 
1683 			int itemStart = qMax(line.from, si.position), itemEnd;
1684 			int itemLength = engine.length(item);
1685 			int glyphsStart = logClusters[itemStart - si.position], glyphsEnd;
1686 			if (line.from + line.length < si.position + itemLength) {
1687 				itemEnd = line.from + line.length;
1688 				glyphsEnd = logClusters[itemEnd - si.position];
1689 			} else {
1690 				itemEnd = si.position + itemLength;
1691 				glyphsEnd = si.num_glyphs;
1692 			}
1693 
1694 			QFixed itemWidth = 0;
1695 			for (int g = glyphsStart; g < glyphsEnd; ++g)
1696 				itemWidth += glyphs.effectiveAdvance(g);
1697 
1698 			if (!_p && _lookupX >= x && _lookupX < x + itemWidth) { // _lookupRequest
1699 				if (_lookupLink) {
1700 					if (currentBlock->lnkIndex() && _lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
1701 						_lookupResult.link = _t->_links.at(currentBlock->lnkIndex() - 1);
1702 					}
1703 				}
1704 				_lookupResult.uponSymbol = true;
1705 				if (_lookupSymbol) {
1706 					QFixed tmpx = rtl ? (x + itemWidth) : x;
1707 					for (int ch = 0, g, itemL = itemEnd - itemStart; ch < itemL;) {
1708 						g = logClusters[itemStart - si.position + ch];
1709 						QFixed gwidth = glyphs.effectiveAdvance(g);
1710 						// ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes
1711 						int ch2 = ch + 1;
1712 						while ((ch2 < itemL) && (g == logClusters[itemStart - si.position + ch2])) {
1713 							++ch2;
1714 						}
1715 						for (int charsCount = (ch2 - ch); ch < ch2; ++ch) {
1716 							QFixed shift1 = QFixed(2 * (charsCount - (ch2 - ch)) + 2) * gwidth / QFixed(2 * charsCount),
1717 								shift2 = QFixed(2 * (charsCount - (ch2 - ch)) + 1) * gwidth / QFixed(2 * charsCount);
1718 							if ((rtl && _lookupX >= tmpx - shift1) ||
1719 								(!rtl && _lookupX < tmpx + shift1)) {
1720 								_lookupResult.symbol = _localFrom + itemStart + ch;
1721 								if ((rtl && _lookupX >= tmpx - shift2) ||
1722 									(!rtl && _lookupX < tmpx + shift2)) {
1723 									_lookupResult.afterSymbol = false;
1724 								} else {
1725 									_lookupResult.afterSymbol = true;
1726 								}
1727 								return false;
1728 							}
1729 						}
1730 						if (rtl) {
1731 							tmpx -= gwidth;
1732 						} else {
1733 							tmpx += gwidth;
1734 						}
1735 					}
1736 					if (itemEnd > itemStart) {
1737 						_lookupResult.symbol = _localFrom + itemEnd - 1;
1738 						_lookupResult.afterSymbol = true;
1739 					} else {
1740 						_lookupResult.symbol = _localFrom + itemStart;
1741 						_lookupResult.afterSymbol = false;
1742 					}
1743 				}
1744 				return false;
1745 			} else if (_p) {
1746 				QTextCharFormat format;
1747 				QTextItemInt gf(glyphs.mid(glyphsStart, glyphsEnd - glyphsStart),
1748 								&_e->fnt, engine.layoutData->string.unicode() + itemStart,
1749 								itemEnd - itemStart, engine.fontEngine(si), format);
1750 				gf.logClusters = logClusters + itemStart - si.position;
1751 				gf.width = itemWidth;
1752 				gf.justified = false;
1753 				gf.initWithScriptItem(si);
1754 
1755 				auto hasSelected = false;
1756 				auto hasNotSelected = true;
1757 				auto selectedRect = QRect();
1758 				if (_localFrom + itemStart < _selection.to && _localFrom + itemEnd > _selection.from) {
1759 					hasSelected = true;
1760 					auto selX = x;
1761 					auto selWidth = itemWidth;
1762 					if (_localFrom + itemStart >= _selection.from && _localFrom + itemEnd <= _selection.to) {
1763 						hasNotSelected = false;
1764 					} else {
1765 						selWidth = 0;
1766 						int itemL = itemEnd - itemStart;
1767 						int selStart = _selection.from - (_localFrom + itemStart), selEnd = _selection.to - (_localFrom + itemStart);
1768 						if (selStart < 0) selStart = 0;
1769 						if (selEnd > itemL) selEnd = itemL;
1770 						for (int ch = 0, g; ch < selEnd;) {
1771 							g = logClusters[itemStart - si.position + ch];
1772 							QFixed gwidth = glyphs.effectiveAdvance(g);
1773 							// ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes
1774 							int ch2 = ch + 1;
1775 							while ((ch2 < itemL) && (g == logClusters[itemStart - si.position + ch2])) {
1776 								++ch2;
1777 							}
1778 							if (ch2 <= selStart) {
1779 								selX += gwidth;
1780 							} else if (ch >= selStart && ch2 <= selEnd) {
1781 								selWidth += gwidth;
1782 							} else {
1783 								int sStart = ch, sEnd = ch2;
1784 								if (ch < selStart) {
1785 									sStart = selStart;
1786 									selX += QFixed(sStart - ch) * gwidth / QFixed(ch2 - ch);
1787 								}
1788 								if (ch2 >= selEnd) {
1789 									sEnd = selEnd;
1790 									selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch);
1791 									break;
1792 								}
1793 								selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch);
1794 							}
1795 							ch = ch2;
1796 						}
1797 					}
1798 					if (rtl) selX = x + itemWidth - (selX - x) - selWidth;
1799 					selectedRect = QRect(selX.toInt(), _y + _yDelta, (selX + selWidth).toInt() - selX.toInt(), _fontHeight);
1800 					fillSelectRange(selX, selX + selWidth);
1801 				}
1802 				if (Q_UNLIKELY(hasSelected)) {
1803 					if (Q_UNLIKELY(hasNotSelected)) {
1804 						auto clippingEnabled = _p->hasClipping();
1805 						auto clippingRegion = _p->clipRegion();
1806 						_p->setClipRect(selectedRect, Qt::IntersectClip);
1807 						_p->setPen(*_currentPenSelected);
1808 						_p->drawTextItem(QPointF(x.toReal(), textY), gf);
1809 						auto externalClipping = clippingEnabled ? clippingRegion : QRegion(QRect((_x - _w).toInt(), _y - _lineHeight, (_x + 2 * _w).toInt(), _y + 2 * _lineHeight));
1810 						_p->setClipRegion(externalClipping - selectedRect);
1811 						_p->setPen(*_currentPen);
1812 						_p->drawTextItem(QPointF(x.toReal(), textY), gf);
1813 						if (clippingEnabled) {
1814 							_p->setClipRegion(clippingRegion);
1815 						} else {
1816 							_p->setClipping(false);
1817 						}
1818 					} else {
1819 						_p->setPen(*_currentPenSelected);
1820 						_p->drawTextItem(QPointF(x.toReal(), textY), gf);
1821 					}
1822 				} else {
1823 					_p->setPen(*_currentPen);
1824 					_p->drawTextItem(QPointF(x.toReal(), textY), gf);
1825 				}
1826 			}
1827 
1828 			x += itemWidth;
1829 		}
1830 		return true;
1831 	}
fillSelectRange(QFixed from,QFixed to)1832 	void fillSelectRange(QFixed from, QFixed to) {
1833 		auto left = from.toInt();
1834 		auto width = to.toInt() - left;
1835 		_p->fillRect(left, _y + _yDelta, width, _fontHeight, _textPalette->selectBg);
1836 	}
1837 
elideSaveBlock(int32 blockIndex,const AbstractBlock * & _endBlock,int32 elideStart,int32 elideWidth)1838 	void elideSaveBlock(int32 blockIndex, const AbstractBlock *&_endBlock, int32 elideStart, int32 elideWidth) {
1839 		if (_elideSavedBlock) {
1840 			restoreAfterElided();
1841 		}
1842 
1843 		_elideSavedIndex = blockIndex;
1844 		auto mutableText = const_cast<String*>(_t);
1845 		_elideSavedBlock = std::move(mutableText->_blocks[blockIndex]);
1846 		mutableText->_blocks[blockIndex] = Block::Text(_t->_st->font, _t->_text, QFIXED_MAX, elideStart, 0, (*_elideSavedBlock)->flags(), (*_elideSavedBlock)->lnkIndex());
1847 		_blocksSize = blockIndex + 1;
1848 		_endBlock = (blockIndex + 1 < _t->_blocks.size() ? _t->_blocks[blockIndex + 1].get() : nullptr);
1849 	}
1850 
setElideBidi(int32 elideStart,int32 elideLen)1851 	void setElideBidi(int32 elideStart, int32 elideLen) {
1852 		int32 newParLength = elideStart + elideLen - _parStart;
1853 		if (newParLength > _parAnalysis.size()) {
1854 			_parAnalysis.resize(newParLength);
1855 		}
1856 		for (int32 i = elideLen; i > 0; --i) {
1857 			_parAnalysis[newParLength - i].bidiLevel = (_parDirection == Qt::RightToLeft) ? 1 : 0;
1858 		}
1859 	}
1860 
prepareElidedLine(QString & lineText,int32 lineStart,int32 & lineLength,const AbstractBlock * & _endBlock,int repeat=0)1861 	void prepareElidedLine(QString &lineText, int32 lineStart, int32 &lineLength, const AbstractBlock *&_endBlock, int repeat = 0) {
1862 		static const auto _Elide = QString::fromLatin1("...");
1863 
1864 		_f = _t->_st->font;
1865 		QStackTextEngine engine(lineText, _f->f);
1866 		engine.option.setTextDirection(_parDirection);
1867 		_e = &engine;
1868 
1869 		eItemize();
1870 
1871 		auto blockIndex = _lineStartBlock;
1872 		auto currentBlock = _t->_blocks[blockIndex].get();
1873 		auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
1874 
1875 		QScriptLine line;
1876 		line.from = lineStart;
1877 		line.length = lineLength;
1878 		eShapeLine(line);
1879 
1880 		auto elideWidth = _f->elidew;
1881 		_wLeft = _w - elideWidth - _elideRemoveFromEnd;
1882 
1883 		int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1);
1884 		int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0, i;
1885 
1886 		for (i = 0; i < nItems; ++i) {
1887 			QScriptItem &si(engine.layoutData->items[firstItem + i]);
1888 			while (nextBlock && nextBlock->from() <= _localFrom + si.position) {
1889 				currentBlock = nextBlock;
1890 				nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
1891 			}
1892 			TextBlockType _type = currentBlock->type();
1893 			if (si.analysis.flags == QScriptAnalysis::Object) {
1894 				if (_type == TextBlockTEmoji || _type == TextBlockTSkip) {
1895 					si.width = currentBlock->f_width() + currentBlock->f_rpadding();
1896 				}
1897 			}
1898 			if (_type == TextBlockTEmoji || _type == TextBlockTSkip || _type == TextBlockTNewline) {
1899 				if (_wLeft < si.width) {
1900 					lineText = lineText.mid(0, currentBlock->from() - _localFrom) + _Elide;
1901 					lineLength = currentBlock->from() + _Elide.size() - _lineStart;
1902 					_selection.to = qMin(_selection.to, currentBlock->from());
1903 					setElideBidi(currentBlock->from(), _Elide.size());
1904 					elideSaveBlock(blockIndex - 1, _endBlock, currentBlock->from(), elideWidth);
1905 					return;
1906 				}
1907 				_wLeft -= si.width;
1908 			} else if (_type == TextBlockTText) {
1909 				unsigned short *logClusters = engine.logClusters(&si);
1910 				QGlyphLayout glyphs = engine.shapedGlyphs(&si);
1911 
1912 				int itemStart = qMax(line.from, si.position), itemEnd;
1913 				int itemLength = engine.length(firstItem + i);
1914 				int glyphsStart = logClusters[itemStart - si.position], glyphsEnd;
1915 				if (line.from + line.length < si.position + itemLength) {
1916 					itemEnd = line.from + line.length;
1917 					glyphsEnd = logClusters[itemEnd - si.position];
1918 				} else {
1919 					itemEnd = si.position + itemLength;
1920 					glyphsEnd = si.num_glyphs;
1921 				}
1922 
1923 				for (auto g = glyphsStart; g < glyphsEnd; ++g) {
1924 					auto adv = glyphs.effectiveAdvance(g);
1925 					if (_wLeft < adv) {
1926 						auto pos = itemStart;
1927 						while (pos < itemEnd && logClusters[pos - si.position] < g) {
1928 							++pos;
1929 						}
1930 
1931 						if (lineText.size() <= pos || repeat > 3) {
1932 							lineText += _Elide;
1933 							lineLength = _localFrom + pos + _Elide.size() - _lineStart;
1934 							_selection.to = qMin(_selection.to, uint16(_localFrom + pos));
1935 							setElideBidi(_localFrom + pos, _Elide.size());
1936 							_blocksSize = blockIndex;
1937 							_endBlock = nextBlock;
1938 						} else {
1939 							lineText = lineText.mid(0, pos);
1940 							lineLength = _localFrom + pos - _lineStart;
1941 							_blocksSize = blockIndex;
1942 							_endBlock = nextBlock;
1943 							prepareElidedLine(lineText, lineStart, lineLength, _endBlock, repeat + 1);
1944 						}
1945 						return;
1946 					} else {
1947 						_wLeft -= adv;
1948 					}
1949 				}
1950 			}
1951 		}
1952 
1953 		int32 elideStart = _localFrom + lineText.size();
1954 		_selection.to = qMin(_selection.to, uint16(elideStart));
1955 		setElideBidi(elideStart, _Elide.size());
1956 
1957 		lineText += _Elide;
1958 		lineLength += _Elide.size();
1959 
1960 		if (!repeat) {
1961 			for (; blockIndex < _blocksSize && _t->_blocks[blockIndex].get() != _endBlock && _t->_blocks[blockIndex]->from() < elideStart; ++blockIndex) {
1962 			}
1963 			if (blockIndex < _blocksSize) {
1964 				elideSaveBlock(blockIndex, _endBlock, elideStart, elideWidth);
1965 			}
1966 		}
1967 	}
1968 
restoreAfterElided()1969 	void restoreAfterElided() {
1970 		if (_elideSavedBlock) {
1971 			const_cast<String*>(_t)->_blocks[_elideSavedIndex] = std::move(*_elideSavedBlock);
1972 		}
1973 	}
1974 
1975 	// COPIED FROM qtextengine.cpp AND MODIFIED
eShapeLine(const QScriptLine & line)1976 	void eShapeLine(const QScriptLine &line) {
1977 		int item = _e->findItem(line.from);
1978 		if (item == -1)
1979 			return;
1980 
1981 		auto end = _e->findItem(line.from + line.length - 1, item);
1982 		auto blockIndex = _lineStartBlock;
1983 		auto currentBlock = _t->_blocks[blockIndex].get();
1984 		auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
1985 		eSetFont(currentBlock);
1986 		for (; item <= end; ++item) {
1987 			QScriptItem &si = _e->layoutData->items[item];
1988 			while (nextBlock && nextBlock->from() <= _localFrom + si.position) {
1989 				currentBlock = nextBlock;
1990 				nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
1991 				eSetFont(currentBlock);
1992 			}
1993 			_e->shape(item);
1994 		}
1995 	}
1996 
applyFlags(int32 flags,const style::font & f)1997 	style::font applyFlags(int32 flags, const style::font &f) {
1998 		if (!flags) {
1999 			return f;
2000 		}
2001 		auto result = f;
2002 		if ((flags & TextBlockFPre) || (flags & TextBlockFCode)) {
2003 			result = result->monospace();
2004 		} else {
2005 			if (flags & TextBlockFBold) {
2006 				result = result->bold();
2007 			} else if (flags & TextBlockFSemibold) {
2008 				result = result->semibold();
2009 			}
2010 			if (flags & TextBlockFItalic) result = result->italic();
2011 			if (flags & TextBlockFUnderline) result = result->underline();
2012 			if (flags & TextBlockFStrikeOut) result = result->strikeout();
2013 			if (flags & TextBlockFTilde) { // tilde fix in OpenSans
2014 				result = result->semibold();
2015 			}
2016 		}
2017 		return result;
2018 	}
2019 
eSetFont(const AbstractBlock * block)2020 	void eSetFont(const AbstractBlock *block) {
2021 		const auto flags = block->flags();
2022 		const auto usedFont = [&] {
2023 			if (const auto index = block->lnkIndex()) {
2024 				const auto active = ClickHandler::showAsActive(
2025 					_t->_links.at(index - 1)
2026 				) || (_textPalette && _textPalette->linkAlwaysActive > 0);
2027 				return active
2028 					? _t->_st->linkFontOver
2029 					: _t->_st->linkFont;
2030 			}
2031 			return _t->_st->font;
2032 		}();
2033 		const auto newFont = applyFlags(flags, usedFont);
2034 		if (newFont != _f) {
2035 			_f = (newFont->family() == _t->_st->font->family())
2036 				? applyFlags(flags | newFont->flags(), _t->_st->font)
2037 				: newFont;
2038 			_e->fnt = _f->f;
2039 			_e->resetFontEngineCache();
2040 		}
2041 	}
2042 
eItemize()2043 	void eItemize() {
2044 		_e->validate();
2045 		if (_e->layoutData->items.size())
2046 			return;
2047 
2048 		int length = _e->layoutData->string.length();
2049 		if (!length)
2050 			return;
2051 
2052 		const ushort *string = reinterpret_cast<const ushort*>(_e->layoutData->string.unicode());
2053 
2054 		auto blockIndex = _lineStartBlock;
2055 		auto currentBlock = _t->_blocks[blockIndex].get();
2056 		auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
2057 
2058 		_e->layoutData->hasBidi = _parHasBidi;
2059 		auto analysis = _parAnalysis.data() + (_localFrom - _parStart);
2060 
2061 		{
2062 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
2063 			QUnicodeTools::ScriptItemArray scriptItems;
2064 			QUnicodeTools::initScripts(_e->layoutData->string, &scriptItems);
2065 			for (int i = 0; i < scriptItems.length(); ++i) {
2066 				const auto &item = scriptItems.at(i);
2067 				int end = i < scriptItems.length() - 1 ? scriptItems.at(i + 1).position : length;
2068 				for (int j = item.position; j < end; ++j)
2069 					analysis[j].script = item.script;
2070 			}
2071 #else // Qt >= 6.0.0
2072 			QVarLengthArray<uchar> scripts(length);
2073 			QUnicodeTools::initScripts(string, length, scripts.data());
2074 			for (int i = 0; i < length; ++i)
2075 				analysis[i].script = scripts.at(i);
2076 #endif // Qt < 6.0.0
2077 		}
2078 
2079 		blockIndex = _lineStartBlock;
2080 		currentBlock = _t->_blocks[blockIndex].get();
2081 		nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
2082 
2083 		auto start = string;
2084 		auto end = start + length;
2085 		while (start < end) {
2086 			while (nextBlock && nextBlock->from() <= _localFrom + (start - string)) {
2087 				currentBlock = nextBlock;
2088 				nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
2089 			}
2090 			auto _type = currentBlock->type();
2091 			if (_type == TextBlockTEmoji || _type == TextBlockTSkip) {
2092 				analysis->script = QChar::Script_Common;
2093 				analysis->flags = QScriptAnalysis::Object;
2094 			} else {
2095 				analysis->flags = QScriptAnalysis::None;
2096 			}
2097 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
2098 			analysis->script = hbscript_to_script(script_to_hbscript(analysis->script)); // retain the old behavior
2099 #endif // Qt < 6.0.0
2100 			++start;
2101 			++analysis;
2102 		}
2103 
2104 		{
2105 			auto i_string = &_e->layoutData->string;
2106 			auto i_analysis = _parAnalysis.data() + (_localFrom - _parStart);
2107 			auto i_items = &_e->layoutData->items;
2108 
2109 			blockIndex = _lineStartBlock;
2110 			currentBlock = _t->_blocks[blockIndex].get();
2111 			nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
2112 			auto startBlock = currentBlock;
2113 
2114 			if (!length) {
2115 				return;
2116 			}
2117 			auto start = 0;
2118 			auto end = start + length;
2119 			for (int i = start + 1; i < end; ++i) {
2120 				while (nextBlock && nextBlock->from() <= _localFrom + i) {
2121 					currentBlock = nextBlock;
2122 					nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
2123 				}
2124 				// According to the unicode spec we should be treating characters in the Common script
2125 				// (punctuation, spaces, etc) as being the same script as the surrounding text for the
2126 				// purpose of splitting up text. This is important because, for example, a fullstop
2127 				// (0x2E) can be used to indicate an abbreviation and so must be treated as part of a
2128 				// word.  Thus it must be passed along with the word in languages that have to calculate
2129 				// word breaks.  For example the thai word "ครม." has no word breaks but the word "ครม"
2130 				// does.
2131 				// Unfortuntely because we split up the strings for both wordwrapping and for setting
2132 				// the font and because Japanese and Chinese are also aliases of the script "Common",
2133 				// doing this would break too many things.  So instead we only pass the full stop
2134 				// along, and nothing else.
2135 				if (currentBlock == startBlock
2136 					&& i_analysis[i].bidiLevel == i_analysis[start].bidiLevel
2137 					&& i_analysis[i].flags == i_analysis[start].flags
2138 					&& (i_analysis[i].script == i_analysis[start].script || i_string->at(i) == QLatin1Char('.'))
2139 //					&& i_analysis[i].flags < QScriptAnalysis::SpaceTabOrObject // only emojis are objects here, no tabs
2140 					&& i - start < _MaxItemLength)
2141 					continue;
2142 				i_items->append(QScriptItem(start, i_analysis[start]));
2143 				start = i;
2144 				startBlock = currentBlock;
2145 			}
2146 			i_items->append(QScriptItem(start, i_analysis[start]));
2147 		}
2148 	}
2149 
eSkipBoundryNeutrals(QScriptAnalysis * analysis,const ushort * unicode,int & sor,int & eor,BidiControl & control,String::TextBlocks::const_iterator i)2150 	QChar::Direction eSkipBoundryNeutrals(QScriptAnalysis *analysis,
2151 											const ushort *unicode,
2152 											int &sor, int &eor, BidiControl &control,
2153 											String::TextBlocks::const_iterator i) {
2154 		String::TextBlocks::const_iterator e = _t->_blocks.cend(), n = i + 1;
2155 
2156 		QChar::Direction dir = control.basicDirection();
2157 		int level = sor > 0 ? analysis[sor - 1].bidiLevel : control.level;
2158 		while (sor <= _parLength) {
2159 			while (i != _parStartBlock && (*i)->from() > _parStart + sor) {
2160 				n = i;
2161 				--i;
2162 			}
2163 			while (n != e && (*n)->from() <= _parStart + sor) {
2164 				i = n;
2165 				++n;
2166 			}
2167 
2168 			TextBlockType _itype = (*i)->type();
2169 			if (eor == _parLength)
2170 				dir = control.basicDirection();
2171 			else if (_itype == TextBlockTEmoji)
2172 				dir = QChar::DirCS;
2173 			else if (_itype == TextBlockTSkip)
2174 				dir = QChar::DirCS;
2175 			else
2176 				dir = QChar::direction(unicode[sor]);
2177 			// Keep skipping DirBN as if it doesn't exist
2178 			if (dir != QChar::DirBN)
2179 				break;
2180 			analysis[sor++].bidiLevel = level;
2181 		}
2182 
2183 		eor = sor;
2184 
2185 		return dir;
2186 	}
2187 
2188 	// creates the next QScript items.
eBidiItemize(QScriptAnalysis * analysis,BidiControl & control)2189 	bool eBidiItemize(QScriptAnalysis *analysis, BidiControl &control) {
2190 		bool rightToLeft = (control.basicDirection() == 1);
2191 		bool hasBidi = rightToLeft;
2192 
2193 		int sor = 0;
2194 		int eor = -1;
2195 
2196 		const ushort *unicode = reinterpret_cast<const ushort*>(_t->_text.unicode()) + _parStart;
2197 		int current = 0;
2198 
2199 		QChar::Direction dir = rightToLeft ? QChar::DirR : QChar::DirL;
2200 		BidiStatus status;
2201 
2202 		String::TextBlocks::const_iterator i = _parStartBlock, e = _t->_blocks.cend(), n = i + 1;
2203 
2204 		QChar::Direction sdir;
2205 		TextBlockType _stype = (*_parStartBlock)->type();
2206 		if (_stype == TextBlockTEmoji)
2207 			sdir = QChar::DirCS;
2208 		else if (_stype == TextBlockTSkip)
2209 			sdir = QChar::DirCS;
2210 		else
2211 			sdir = QChar::direction(*unicode);
2212 		if (sdir != QChar::DirL && sdir != QChar::DirR && sdir != QChar::DirEN && sdir != QChar::DirAN)
2213 			sdir = QChar::DirON;
2214 		else
2215 			dir = QChar::DirON;
2216 
2217 		status.eor = sdir;
2218 		status.lastStrong = rightToLeft ? QChar::DirR : QChar::DirL;
2219 		status.last = status.lastStrong;
2220 		status.dir = sdir;
2221 
2222 		while (current <= _parLength) {
2223 			while (n != e && (*n)->from() <= _parStart + current) {
2224 				i = n;
2225 				++n;
2226 			}
2227 
2228 			QChar::Direction dirCurrent;
2229 			TextBlockType _itype = (*i)->type();
2230 			if (current == (int)_parLength)
2231 				dirCurrent = control.basicDirection();
2232 			else if (_itype == TextBlockTEmoji)
2233 				dirCurrent = QChar::DirCS;
2234 			else if (_itype == TextBlockTSkip)
2235 				dirCurrent = QChar::DirCS;
2236 			else
2237 				dirCurrent = QChar::direction(unicode[current]);
2238 
2239 			switch (dirCurrent) {
2240 
2241 				// embedding and overrides (X1-X9 in the BiDi specs)
2242 			case QChar::DirRLE:
2243 			case QChar::DirRLO:
2244 			case QChar::DirLRE:
2245 			case QChar::DirLRO:
2246 				{
2247 					bool rtl = (dirCurrent == QChar::DirRLE || dirCurrent == QChar::DirRLO);
2248 					hasBidi |= rtl;
2249 					bool override = (dirCurrent == QChar::DirLRO || dirCurrent == QChar::DirRLO);
2250 
2251 					unsigned int level = control.level+1;
2252 					if ((level%2 != 0) == rtl) ++level;
2253 					if (level < _MaxBidiLevel) {
2254 						eor = current-1;
2255 						eAppendItems(analysis, sor, eor, control, dir);
2256 						eor = current;
2257 						control.embed(rtl, override);
2258 						QChar::Direction edir = (rtl ? QChar::DirR : QChar::DirL);
2259 						dir = status.eor = edir;
2260 						status.lastStrong = edir;
2261 					}
2262 					break;
2263 				}
2264 			case QChar::DirPDF:
2265 				{
2266 					if (control.canPop()) {
2267 						if (dir != control.direction()) {
2268 							eor = current-1;
2269 							eAppendItems(analysis, sor, eor, control, dir);
2270 							dir = control.direction();
2271 						}
2272 						eor = current;
2273 						eAppendItems(analysis, sor, eor, control, dir);
2274 						control.pdf();
2275 						dir = QChar::DirON; status.eor = QChar::DirON;
2276 						status.last = control.direction();
2277 						if (control.override)
2278 							dir = control.direction();
2279 						else
2280 							dir = QChar::DirON;
2281 						status.lastStrong = control.direction();
2282 					}
2283 					break;
2284 				}
2285 
2286 				// strong types
2287 			case QChar::DirL:
2288 				if(dir == QChar::DirON)
2289 					dir = QChar::DirL;
2290 				switch(status.last)
2291 					{
2292 					case QChar::DirL:
2293 						eor = current; status.eor = QChar::DirL; break;
2294 					case QChar::DirR:
2295 					case QChar::DirAL:
2296 					case QChar::DirEN:
2297 					case QChar::DirAN:
2298 						if (eor >= 0) {
2299 							eAppendItems(analysis, sor, eor, control, dir);
2300 							status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i);
2301 						} else {
2302 							eor = current; status.eor = dir;
2303 						}
2304 						break;
2305 					case QChar::DirES:
2306 					case QChar::DirET:
2307 					case QChar::DirCS:
2308 					case QChar::DirBN:
2309 					case QChar::DirB:
2310 					case QChar::DirS:
2311 					case QChar::DirWS:
2312 					case QChar::DirON:
2313 						if(dir != QChar::DirL) {
2314 							//last stuff takes embedding dir
2315 							if(control.direction() == QChar::DirR) {
2316 								if(status.eor != QChar::DirR) {
2317 									// AN or EN
2318 									eAppendItems(analysis, sor, eor, control, dir);
2319 									status.eor = QChar::DirON;
2320 									dir = QChar::DirR;
2321 								}
2322 								eor = current - 1;
2323 								eAppendItems(analysis, sor, eor, control, dir);
2324 								status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i);
2325 							} else {
2326 								if(status.eor != QChar::DirL) {
2327 									eAppendItems(analysis, sor, eor, control, dir);
2328 									status.eor = QChar::DirON;
2329 									dir = QChar::DirL;
2330 								} else {
2331 									eor = current; status.eor = QChar::DirL; break;
2332 								}
2333 							}
2334 						} else {
2335 							eor = current; status.eor = QChar::DirL;
2336 						}
2337 					default:
2338 						break;
2339 					}
2340 				status.lastStrong = QChar::DirL;
2341 				break;
2342 			case QChar::DirAL:
2343 			case QChar::DirR:
2344 				hasBidi = true;
2345 				if(dir == QChar::DirON) dir = QChar::DirR;
2346 				switch(status.last)
2347 					{
2348 					case QChar::DirL:
2349 					case QChar::DirEN:
2350 					case QChar::DirAN:
2351 						if (eor >= 0)
2352 							eAppendItems(analysis, sor, eor, control, dir);
2353 						// fall through
2354 					case QChar::DirR:
2355 					case QChar::DirAL:
2356 						dir = QChar::DirR; eor = current; status.eor = QChar::DirR; break;
2357 					case QChar::DirES:
2358 					case QChar::DirET:
2359 					case QChar::DirCS:
2360 					case QChar::DirBN:
2361 					case QChar::DirB:
2362 					case QChar::DirS:
2363 					case QChar::DirWS:
2364 					case QChar::DirON:
2365 						if(status.eor != QChar::DirR && status.eor != QChar::DirAL) {
2366 							//last stuff takes embedding dir
2367 							if(control.direction() == QChar::DirR
2368 							   || status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) {
2369 								eAppendItems(analysis, sor, eor, control, dir);
2370 								dir = QChar::DirR; status.eor = QChar::DirON;
2371 								eor = current;
2372 							} else {
2373 								eor = current - 1;
2374 								eAppendItems(analysis, sor, eor, control, dir);
2375 								dir = QChar::DirR; status.eor = QChar::DirON;
2376 							}
2377 						} else {
2378 							eor = current; status.eor = QChar::DirR;
2379 						}
2380 					default:
2381 						break;
2382 					}
2383 				status.lastStrong = dirCurrent;
2384 				break;
2385 
2386 				// weak types:
2387 
2388 			case QChar::DirNSM:
2389 				if (eor == current-1)
2390 					eor = current;
2391 				break;
2392 			case QChar::DirEN:
2393 				// if last strong was AL change EN to AN
2394 				if(status.lastStrong != QChar::DirAL) {
2395 					if(dir == QChar::DirON) {
2396 						if(status.lastStrong == QChar::DirL)
2397 							dir = QChar::DirL;
2398 						else
2399 							dir = QChar::DirEN;
2400 					}
2401 					switch(status.last)
2402 						{
2403 						case QChar::DirET:
2404 							if (status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) {
2405 								eAppendItems(analysis, sor, eor, control, dir);
2406 								status.eor = QChar::DirON;
2407 								dir = QChar::DirAN;
2408 							}
2409 							[[fallthrough]];
2410 						case QChar::DirEN:
2411 						case QChar::DirL:
2412 							eor = current;
2413 							status.eor = dirCurrent;
2414 							break;
2415 						case QChar::DirR:
2416 						case QChar::DirAL:
2417 						case QChar::DirAN:
2418 							if (eor >= 0)
2419 								eAppendItems(analysis, sor, eor, control, dir);
2420 							else
2421 								eor = current;
2422 							status.eor = QChar::DirEN;
2423 							dir = QChar::DirAN;
2424 							break;
2425 						case QChar::DirES:
2426 						case QChar::DirCS:
2427 							if(status.eor == QChar::DirEN || dir == QChar::DirAN) {
2428 								eor = current; break;
2429 							}
2430 							[[fallthrough]];
2431 						case QChar::DirBN:
2432 						case QChar::DirB:
2433 						case QChar::DirS:
2434 						case QChar::DirWS:
2435 						case QChar::DirON:
2436 							if(status.eor == QChar::DirR) {
2437 								// neutrals go to R
2438 								eor = current - 1;
2439 								eAppendItems(analysis, sor, eor, control, dir);
2440 								status.eor = QChar::DirEN;
2441 								dir = QChar::DirAN;
2442 							}
2443 							else if(status.eor == QChar::DirL ||
2444 									 (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) {
2445 								eor = current; status.eor = dirCurrent;
2446 							} else {
2447 								// numbers on both sides, neutrals get right to left direction
2448 								if(dir != QChar::DirL) {
2449 									eAppendItems(analysis, sor, eor, control, dir);
2450 									status.eor = QChar::DirON;
2451 									eor = current - 1;
2452 									dir = QChar::DirR;
2453 									eAppendItems(analysis, sor, eor, control, dir);
2454 									status.eor = QChar::DirON;
2455 									dir = QChar::DirAN;
2456 								} else {
2457 									eor = current; status.eor = dirCurrent;
2458 								}
2459 							}
2460 							[[fallthrough]];
2461 						default:
2462 							break;
2463 						}
2464 					break;
2465 				}
2466 				[[fallthrough]];
2467 			case QChar::DirAN:
2468 				hasBidi = true;
2469 				dirCurrent = QChar::DirAN;
2470 				if(dir == QChar::DirON) dir = QChar::DirAN;
2471 				switch(status.last)
2472 					{
2473 					case QChar::DirL:
2474 					case QChar::DirAN:
2475 						eor = current; status.eor = QChar::DirAN;
2476 						break;
2477 					case QChar::DirR:
2478 					case QChar::DirAL:
2479 					case QChar::DirEN:
2480 						if (eor >= 0){
2481 							eAppendItems(analysis, sor, eor, control, dir);
2482 						} else {
2483 							eor = current;
2484 						}
2485 						dir = QChar::DirAN; status.eor = QChar::DirAN;
2486 						break;
2487 					case QChar::DirCS:
2488 						if(status.eor == QChar::DirAN) {
2489 							eor = current; break;
2490 						}
2491 						[[fallthrough]];
2492 					case QChar::DirES:
2493 					case QChar::DirET:
2494 					case QChar::DirBN:
2495 					case QChar::DirB:
2496 					case QChar::DirS:
2497 					case QChar::DirWS:
2498 					case QChar::DirON:
2499 						if(status.eor == QChar::DirR) {
2500 							// neutrals go to R
2501 							eor = current - 1;
2502 							eAppendItems(analysis, sor, eor, control, dir);
2503 							status.eor = QChar::DirAN;
2504 							dir = QChar::DirAN;
2505 						} else if(status.eor == QChar::DirL ||
2506 								   (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) {
2507 							eor = current; status.eor = dirCurrent;
2508 						} else {
2509 							// numbers on both sides, neutrals get right to left direction
2510 							if(dir != QChar::DirL) {
2511 								eAppendItems(analysis, sor, eor, control, dir);
2512 								status.eor = QChar::DirON;
2513 								eor = current - 1;
2514 								dir = QChar::DirR;
2515 								eAppendItems(analysis, sor, eor, control, dir);
2516 								status.eor = QChar::DirAN;
2517 								dir = QChar::DirAN;
2518 							} else {
2519 								eor = current; status.eor = dirCurrent;
2520 							}
2521 						}
2522 						[[fallthrough]];
2523 					default:
2524 						break;
2525 					}
2526 				break;
2527 			case QChar::DirES:
2528 			case QChar::DirCS:
2529 				break;
2530 			case QChar::DirET:
2531 				if(status.last == QChar::DirEN) {
2532 					dirCurrent = QChar::DirEN;
2533 					eor = current; status.eor = dirCurrent;
2534 				}
2535 				break;
2536 
2537 				// boundary neutrals should be ignored
2538 			case QChar::DirBN:
2539 				break;
2540 				// neutrals
2541 			case QChar::DirB:
2542 				// ### what do we do with newline and paragraph separators that come to here?
2543 				break;
2544 			case QChar::DirS:
2545 				// ### implement rule L1
2546 				break;
2547 			case QChar::DirWS:
2548 			case QChar::DirON:
2549 				break;
2550 			default:
2551 				break;
2552 			}
2553 
2554 			if(current >= (int)_parLength) break;
2555 
2556 			// set status.last as needed.
2557 			switch(dirCurrent) {
2558 			case QChar::DirET:
2559 			case QChar::DirES:
2560 			case QChar::DirCS:
2561 			case QChar::DirS:
2562 			case QChar::DirWS:
2563 			case QChar::DirON:
2564 				switch(status.last)
2565 				{
2566 				case QChar::DirL:
2567 				case QChar::DirR:
2568 				case QChar::DirAL:
2569 				case QChar::DirEN:
2570 				case QChar::DirAN:
2571 					status.last = dirCurrent;
2572 					break;
2573 				default:
2574 					status.last = QChar::DirON;
2575 				}
2576 				break;
2577 			case QChar::DirNSM:
2578 			case QChar::DirBN:
2579 				// ignore these
2580 				break;
2581 			case QChar::DirLRO:
2582 			case QChar::DirLRE:
2583 				status.last = QChar::DirL;
2584 				break;
2585 			case QChar::DirRLO:
2586 			case QChar::DirRLE:
2587 				status.last = QChar::DirR;
2588 				break;
2589 			case QChar::DirEN:
2590 				if (status.last == QChar::DirL) {
2591 					status.last = QChar::DirL;
2592 					break;
2593 				}
2594 				[[fallthrough]];
2595 			default:
2596 				status.last = dirCurrent;
2597 			}
2598 
2599 			++current;
2600 		}
2601 
2602 		eor = current - 1; // remove dummy char
2603 
2604 		if (sor <= eor)
2605 			eAppendItems(analysis, sor, eor, control, dir);
2606 
2607 		return hasBidi;
2608 	}
2609 
2610 private:
applyBlockProperties(const AbstractBlock * block)2611 	void applyBlockProperties(const AbstractBlock *block) {
2612 		eSetFont(block);
2613 		if (_p) {
2614 			if (block->lnkIndex()) {
2615 				_currentPen = &_textPalette->linkFg->p;
2616 				_currentPenSelected = &_textPalette->selectLinkFg->p;
2617 			} else if ((block->flags() & TextBlockFCode) || (block->flags() & TextBlockFPre)) {
2618 				_currentPen = &_textPalette->monoFg->p;
2619 				_currentPenSelected = &_textPalette->selectMonoFg->p;
2620 			} else {
2621 				_currentPen = &_originalPen;
2622 				_currentPenSelected = &_originalPenSelected;
2623 			}
2624 		}
2625 	}
2626 
2627 	Painter *_p = nullptr;
2628 	const style::TextPalette *_textPalette = nullptr;
2629 	const String *_t = nullptr;
2630 	bool _elideLast = false;
2631 	bool _breakEverywhere = false;
2632 	int _elideRemoveFromEnd = 0;
2633 	style::align _align = style::al_topleft;
2634 	const QPen _originalPen;
2635 	QPen _originalPenSelected;
2636 	const QPen *_currentPen = nullptr;
2637 	const QPen *_currentPenSelected = nullptr;
2638 	int _yFrom = 0;
2639 	int _yTo = 0;
2640 	int _yToElide = 0;
2641 	TextSelection _selection = { 0, 0 };
2642 	bool _fullWidthSelection = true;
2643 	const QChar *_str = nullptr;
2644 
2645 	// current paragraph data
2646 	String::TextBlocks::const_iterator _parStartBlock;
2647 	Qt::LayoutDirection _parDirection;
2648 	int _parStart = 0;
2649 	int _parLength = 0;
2650 	bool _parHasBidi = false;
2651 	QVarLengthArray<QScriptAnalysis, 4096> _parAnalysis;
2652 
2653 	// current line data
2654 	QTextEngine *_e = nullptr;
2655 	style::font _f;
2656 	QFixed _x, _w, _wLeft, _last_rPadding;
2657 	int32 _y, _yDelta, _lineHeight, _fontHeight;
2658 
2659 	// elided hack support
2660 	int _blocksSize = 0;
2661 	int _elideSavedIndex = 0;
2662 	std::optional<Block> _elideSavedBlock;
2663 
2664 	int _lineStart = 0;
2665 	int _localFrom = 0;
2666 	int _lineStartBlock = 0;
2667 
2668 	// link and symbol resolve
2669 	QFixed _lookupX = 0;
2670 	int _lookupY = 0;
2671 	bool _lookupSymbol = false;
2672 	bool _lookupLink = false;
2673 	StateRequest _lookupRequest;
2674 	StateResult _lookupResult;
2675 
2676 };
2677 
String(int32 minResizeWidth)2678 String::String(int32 minResizeWidth) : _minResizeWidth(minResizeWidth) {
2679 }
2680 
String(const style::TextStyle & st,const QString & text,const TextParseOptions & options,int32 minResizeWidth,bool richText)2681 String::String(const style::TextStyle &st, const QString &text, const TextParseOptions &options, int32 minResizeWidth, bool richText)
2682 : _minResizeWidth(minResizeWidth) {
2683 	if (richText) {
2684 		setRichText(st, text, options);
2685 	} else {
2686 		setText(st, text, options);
2687 	}
2688 }
2689 
setText(const style::TextStyle & st,const QString & text,const TextParseOptions & options)2690 void String::setText(const style::TextStyle &st, const QString &text, const TextParseOptions &options) {
2691 	_st = &st;
2692 	clear();
2693 	{
2694 		Parser parser(this, text, options, {});
2695 	}
2696 	recountNaturalSize(true, options.dir);
2697 }
2698 
recountNaturalSize(bool initial,Qt::LayoutDirection optionsDir)2699 void String::recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir) {
2700 	NewlineBlock *lastNewline = 0;
2701 
2702 	_maxWidth = _minHeight = 0;
2703 	int32 lineHeight = 0;
2704 	int32 lastNewlineStart = 0;
2705 	QFixed _width = 0, last_rBearing = 0, last_rPadding = 0;
2706 	for (auto &block : _blocks) {
2707 		auto b = block.get();
2708 		auto _btype = b->type();
2709 		auto blockHeight = countBlockHeight(b, _st);
2710 		if (_btype == TextBlockTNewline) {
2711 			if (!lineHeight) lineHeight = blockHeight;
2712 			if (initial) {
2713 				Qt::LayoutDirection dir = optionsDir;
2714 				if (dir == Qt::LayoutDirectionAuto) {
2715 					dir = StringDirection(_text, lastNewlineStart, b->from());
2716 				}
2717 				if (lastNewline) {
2718 					lastNewline->_nextDir = dir;
2719 				} else {
2720 					_startDir = dir;
2721 				}
2722 			}
2723 			lastNewlineStart = b->from();
2724 			lastNewline = &block.unsafe<NewlineBlock>();
2725 
2726 			_minHeight += lineHeight;
2727 			lineHeight = 0;
2728 			last_rBearing = b->f_rbearing();
2729 			last_rPadding = b->f_rpadding();
2730 
2731 			accumulate_max(_maxWidth, _width);
2732 			_width = (b->f_width() - last_rBearing);
2733 			continue;
2734 		}
2735 
2736 		auto b__f_rbearing = b->f_rbearing(); // cache
2737 
2738 		// We need to accumulate max width after each block, because
2739 		// some blocks have width less than -1 * previous right bearing.
2740 		// In that cases the _width gets _smaller_ after moving to the next block.
2741 		//
2742 		// But when we layout block and we're sure that _maxWidth is enough
2743 		// for all the blocks to fit on their line we check each block, even the
2744 		// intermediate one with a large negative right bearing.
2745 		accumulate_max(_maxWidth, _width);
2746 
2747 		_width += last_rBearing + (last_rPadding + b->f_width() - b__f_rbearing);
2748 		lineHeight = qMax(lineHeight, blockHeight);
2749 
2750 		last_rBearing = b__f_rbearing;
2751 		last_rPadding = b->f_rpadding();
2752 		continue;
2753 	}
2754 	if (initial) {
2755 		Qt::LayoutDirection dir = optionsDir;
2756 		if (dir == Qt::LayoutDirectionAuto) {
2757 			dir = StringDirection(_text, lastNewlineStart, _text.size());
2758 		}
2759 		if (lastNewline) {
2760 			lastNewline->_nextDir = dir;
2761 		} else {
2762 			_startDir = dir;
2763 		}
2764 	}
2765 	if (_width > 0) {
2766 		if (!lineHeight) lineHeight = countBlockHeight(_blocks.back().get(), _st);
2767 		_minHeight += lineHeight;
2768 		accumulate_max(_maxWidth, _width);
2769 	}
2770 }
2771 
countMaxMonospaceWidth() const2772 int String::countMaxMonospaceWidth() const {
2773 	auto result = QFixed();
2774 	auto paragraphWidth = QFixed();
2775 	auto fullMonospace = true;
2776 	QFixed _width = 0, last_rBearing = 0, last_rPadding = 0;
2777 	for (auto &block : _blocks) {
2778 		auto b = block.get();
2779 		auto _btype = b->type();
2780 		if (_btype == TextBlockTNewline) {
2781 			last_rBearing = b->f_rbearing();
2782 			last_rPadding = b->f_rpadding();
2783 
2784 			if (fullMonospace) {
2785 				accumulate_max(paragraphWidth, _width);
2786 				accumulate_max(result, paragraphWidth);
2787 				paragraphWidth = 0;
2788 			} else {
2789 				fullMonospace = true;
2790 			}
2791 			_width = (b->f_width() - last_rBearing);
2792 			continue;
2793 		}
2794 		if (!(b->flags() & (TextBlockFPre | TextBlockFCode))
2795 			&& (b->type() != TextBlockTSkip)) {
2796 			fullMonospace = false;
2797 		}
2798 		auto b__f_rbearing = b->f_rbearing(); // cache
2799 
2800 		// We need to accumulate max width after each block, because
2801 		// some blocks have width less than -1 * previous right bearing.
2802 		// In that cases the _width gets _smaller_ after moving to the next block.
2803 		//
2804 		// But when we layout block and we're sure that _maxWidth is enough
2805 		// for all the blocks to fit on their line we check each block, even the
2806 		// intermediate one with a large negative right bearing.
2807 		if (fullMonospace) {
2808 			accumulate_max(paragraphWidth, _width);
2809 		}
2810 		_width += last_rBearing + (last_rPadding + b->f_width() - b__f_rbearing);
2811 
2812 		last_rBearing = b__f_rbearing;
2813 		last_rPadding = b->f_rpadding();
2814 		continue;
2815 	}
2816 	if (_width > 0 && fullMonospace) {
2817 		accumulate_max(paragraphWidth, _width);
2818 		accumulate_max(result, paragraphWidth);
2819 	}
2820 	return result.ceil().toInt();
2821 }
2822 
setMarkedText(const style::TextStyle & st,const TextWithEntities & textWithEntities,const TextParseOptions & options,const std::any & context)2823 void String::setMarkedText(const style::TextStyle &st, const TextWithEntities &textWithEntities, const TextParseOptions &options, const std::any &context) {
2824 	_st = &st;
2825 	clear();
2826 	{
2827 		// utf codes of the text display for emoji extraction
2828 //		auto text = textWithEntities.text;
2829 //		auto newText = QString();
2830 //		newText.reserve(8 * text.size());
2831 //		newText.append("\t{ ");
2832 //		for (const QChar *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) {
2833 //			if (*ch == TextCommand) {
2834 //				break;
2835 //			} else if (IsNewline(*ch)) {
2836 //				newText.append("},").append(*ch).append("\t{ ");
2837 //			} else {
2838 //				if (ch->isHighSurrogate() || ch->isLowSurrogate()) {
2839 //					if (ch->isHighSurrogate() && (ch + 1 != e) && ((ch + 1)->isLowSurrogate())) {
2840 //						newText.append("0x").append(QString::number((uint32(ch->unicode()) << 16) | uint32((ch + 1)->unicode()), 16).toUpper()).append("U, ");
2841 //						++ch;
2842 //					} else {
2843 //						newText.append("BADx").append(QString::number(ch->unicode(), 16).toUpper()).append("U, ");
2844 //					}
2845 //				} else {
2846 //					newText.append("0x").append(QString::number(ch->unicode(), 16).toUpper()).append("U, ");
2847 //				}
2848 //			}
2849 //		}
2850 //		newText.append("},\n\n").append(text);
2851 //		Parser parser(this, { newText, EntitiesInText() }, options, context);
2852 
2853 		Parser parser(this, textWithEntities, options, context);
2854 	}
2855 	recountNaturalSize(true, options.dir);
2856 }
2857 
setRichText(const style::TextStyle & st,const QString & text,TextParseOptions options)2858 void String::setRichText(const style::TextStyle &st, const QString &text, TextParseOptions options) {
2859 	options.flags |= TextParseRichText;
2860 	setText(st, text, options);
2861 }
2862 
setLink(uint16 lnkIndex,const ClickHandlerPtr & lnk)2863 void String::setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk) {
2864 	if (!lnkIndex || lnkIndex > _links.size()) return;
2865 	_links[lnkIndex - 1] = lnk;
2866 }
2867 
hasLinks() const2868 bool String::hasLinks() const {
2869 	return !_links.isEmpty();
2870 }
2871 
hasSkipBlock() const2872 bool String::hasSkipBlock() const {
2873 	return _blocks.empty() ? false : _blocks.back()->type() == TextBlockTSkip;
2874 }
2875 
updateSkipBlock(int width,int height)2876 bool String::updateSkipBlock(int width, int height) {
2877 	if (!_blocks.empty() && _blocks.back()->type() == TextBlockTSkip) {
2878 		const auto block = static_cast<SkipBlock*>(_blocks.back().get());
2879 		if (block->width() == width && block->height() == height) {
2880 			return false;
2881 		}
2882 		_text.resize(block->from());
2883 		_blocks.pop_back();
2884 	}
2885 	_text.push_back('_');
2886 	_blocks.push_back(Block::Skip(
2887 		_st->font,
2888 		_text,
2889 		_text.size() - 1,
2890 		width,
2891 		height,
2892 		0));
2893 	recountNaturalSize(false);
2894 	return true;
2895 }
2896 
removeSkipBlock()2897 bool String::removeSkipBlock() {
2898 	if (_blocks.empty() || _blocks.back()->type() != TextBlockTSkip) {
2899 		return false;
2900 	}
2901 	_text.resize(_blocks.back()->from());
2902 	_blocks.pop_back();
2903 	recountNaturalSize(false);
2904 	return true;
2905 }
2906 
countWidth(int width,bool breakEverywhere) const2907 int String::countWidth(int width, bool breakEverywhere) const {
2908 	if (QFixed(width) >= _maxWidth) {
2909 		return _maxWidth.ceil().toInt();
2910 	}
2911 
2912 	QFixed maxLineWidth = 0;
2913 	enumerateLines(width, breakEverywhere, [&](QFixed lineWidth, int lineHeight) {
2914 		if (lineWidth > maxLineWidth) {
2915 			maxLineWidth = lineWidth;
2916 		}
2917 	});
2918 	return maxLineWidth.ceil().toInt();
2919 }
2920 
countHeight(int width,bool breakEverywhere) const2921 int String::countHeight(int width, bool breakEverywhere) const {
2922 	if (QFixed(width) >= _maxWidth) {
2923 		return _minHeight;
2924 	}
2925 	int result = 0;
2926 	enumerateLines(width, breakEverywhere, [&](QFixed lineWidth, int lineHeight) {
2927 		result += lineHeight;
2928 	});
2929 	return result;
2930 }
2931 
countLineWidths(int width,QVector<int> * lineWidths,bool breakEverywhere) const2932 void String::countLineWidths(int width, QVector<int> *lineWidths, bool breakEverywhere) const {
2933 	enumerateLines(width, breakEverywhere, [&](QFixed lineWidth, int lineHeight) {
2934 		lineWidths->push_back(lineWidth.ceil().toInt());
2935 	});
2936 }
2937 
2938 template <typename Callback>
enumerateLines(int w,bool breakEverywhere,Callback callback) const2939 void String::enumerateLines(
2940 		int w,
2941 		bool breakEverywhere,
2942 		Callback callback) const {
2943 	QFixed width = w;
2944 	if (width < _minResizeWidth) width = _minResizeWidth;
2945 
2946 	int lineHeight = 0;
2947 	QFixed widthLeft = width, last_rBearing = 0, last_rPadding = 0;
2948 	bool longWordLine = true;
2949 	for (auto &b : _blocks) {
2950 		auto _btype = b->type();
2951 		int blockHeight = countBlockHeight(b.get(), _st);
2952 
2953 		if (_btype == TextBlockTNewline) {
2954 			if (!lineHeight) lineHeight = blockHeight;
2955 			callback(width - widthLeft, lineHeight);
2956 
2957 			lineHeight = 0;
2958 			last_rBearing = b->f_rbearing();
2959 			last_rPadding = b->f_rpadding();
2960 			widthLeft = width - (b->f_width() - last_rBearing);
2961 
2962 			longWordLine = true;
2963 			continue;
2964 		}
2965 		auto b__f_rbearing = b->f_rbearing();
2966 		auto newWidthLeft = widthLeft - last_rBearing - (last_rPadding + b->f_width() - b__f_rbearing);
2967 		if (newWidthLeft >= 0) {
2968 			last_rBearing = b__f_rbearing;
2969 			last_rPadding = b->f_rpadding();
2970 			widthLeft = newWidthLeft;
2971 
2972 			lineHeight = qMax(lineHeight, blockHeight);
2973 
2974 			longWordLine = false;
2975 			continue;
2976 		}
2977 
2978 		if (_btype == TextBlockTText) {
2979 			const auto t = &b.unsafe<TextBlock>();
2980 			if (t->_words.isEmpty()) { // no words in this block, spaces only => layout this block in the same line
2981 				last_rPadding += b->f_rpadding();
2982 
2983 				lineHeight = qMax(lineHeight, blockHeight);
2984 
2985 				longWordLine = false;
2986 				continue;
2987 			}
2988 
2989 			auto f_wLeft = widthLeft;
2990 			int f_lineHeight = lineHeight;
2991 			for (auto j = t->_words.cbegin(), e = t->_words.cend(), f = j; j != e; ++j) {
2992 				bool wordEndsHere = (j->f_width() >= 0);
2993 				auto j_width = wordEndsHere ? j->f_width() : -j->f_width();
2994 
2995 				auto newWidthLeft = widthLeft - last_rBearing - (last_rPadding + j_width - j->f_rbearing());
2996 				if (newWidthLeft >= 0) {
2997 					last_rBearing = j->f_rbearing();
2998 					last_rPadding = j->f_rpadding();
2999 					widthLeft = newWidthLeft;
3000 
3001 					lineHeight = qMax(lineHeight, blockHeight);
3002 
3003 					if (wordEndsHere) {
3004 						longWordLine = false;
3005 					}
3006 					if (wordEndsHere || longWordLine) {
3007 						f_wLeft = widthLeft;
3008 						f_lineHeight = lineHeight;
3009 						f = j + 1;
3010 					}
3011 					continue;
3012 				}
3013 
3014 				if (f != j && !breakEverywhere) {
3015 					j = f;
3016 					widthLeft = f_wLeft;
3017 					lineHeight = f_lineHeight;
3018 					j_width = (j->f_width() >= 0) ? j->f_width() : -j->f_width();
3019 				}
3020 
3021 				callback(width - widthLeft, lineHeight);
3022 
3023 				lineHeight = qMax(0, blockHeight);
3024 				last_rBearing = j->f_rbearing();
3025 				last_rPadding = j->f_rpadding();
3026 				widthLeft = width - (j_width - last_rBearing);
3027 
3028 				longWordLine = !wordEndsHere;
3029 				f = j + 1;
3030 				f_wLeft = widthLeft;
3031 				f_lineHeight = lineHeight;
3032 			}
3033 			continue;
3034 		}
3035 
3036 		callback(width - widthLeft, lineHeight);
3037 
3038 		lineHeight = qMax(0, blockHeight);
3039 		last_rBearing = b__f_rbearing;
3040 		last_rPadding = b->f_rpadding();
3041 		widthLeft = width - (b->f_width() - last_rBearing);
3042 
3043 		longWordLine = true;
3044 		continue;
3045 	}
3046 	if (widthLeft < width) {
3047 		callback(width - widthLeft, lineHeight);
3048 	}
3049 }
3050 
draw(Painter & painter,int32 left,int32 top,int32 w,style::align align,int32 yFrom,int32 yTo,TextSelection selection,bool fullWidthSelection) const3051 void String::draw(Painter &painter, int32 left, int32 top, int32 w, style::align align, int32 yFrom, int32 yTo, TextSelection selection, bool fullWidthSelection) const {
3052 //	painter.fillRect(QRect(left, top, w, countHeight(w)), QColor(0, 0, 0, 32)); // debug
3053 	Renderer p(&painter, this);
3054 	p.draw(left, top, w, align, yFrom, yTo, selection, fullWidthSelection);
3055 }
3056 
drawElided(Painter & painter,int32 left,int32 top,int32 w,int32 lines,style::align align,int32 yFrom,int32 yTo,int32 removeFromEnd,bool breakEverywhere,TextSelection selection) const3057 void String::drawElided(Painter &painter, int32 left, int32 top, int32 w, int32 lines, style::align align, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) const {
3058 //	painter.fillRect(QRect(left, top, w, countHeight(w)), QColor(0, 0, 0, 32)); // debug
3059 	Renderer p(&painter, this);
3060 	p.drawElided(left, top, w, align, lines, yFrom, yTo, removeFromEnd, breakEverywhere, selection);
3061 }
3062 
drawLeft(Painter & p,int32 left,int32 top,int32 width,int32 outerw,style::align align,int32 yFrom,int32 yTo,TextSelection selection) const3063 void String::drawLeft(Painter &p, int32 left, int32 top, int32 width, int32 outerw, style::align align, int32 yFrom, int32 yTo, TextSelection selection) const {
3064 	draw(p, style::RightToLeft() ? (outerw - left - width) : left, top, width, align, yFrom, yTo, selection);
3065 }
3066 
drawLeftElided(Painter & p,int32 left,int32 top,int32 width,int32 outerw,int32 lines,style::align align,int32 yFrom,int32 yTo,int32 removeFromEnd,bool breakEverywhere,TextSelection selection) const3067 void String::drawLeftElided(Painter &p, int32 left, int32 top, int32 width, int32 outerw, int32 lines, style::align align, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) const {
3068 	drawElided(p, style::RightToLeft() ? (outerw - left - width) : left, top, width, lines, align, yFrom, yTo, removeFromEnd, breakEverywhere, selection);
3069 }
3070 
drawRight(Painter & p,int32 right,int32 top,int32 width,int32 outerw,style::align align,int32 yFrom,int32 yTo,TextSelection selection) const3071 void String::drawRight(Painter &p, int32 right, int32 top, int32 width, int32 outerw, style::align align, int32 yFrom, int32 yTo, TextSelection selection) const {
3072 	draw(p, style::RightToLeft() ? right : (outerw - right - width), top, width, align, yFrom, yTo, selection);
3073 }
3074 
drawRightElided(Painter & p,int32 right,int32 top,int32 width,int32 outerw,int32 lines,style::align align,int32 yFrom,int32 yTo,int32 removeFromEnd,bool breakEverywhere,TextSelection selection) const3075 void String::drawRightElided(Painter &p, int32 right, int32 top, int32 width, int32 outerw, int32 lines, style::align align, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) const {
3076 	drawElided(p, style::RightToLeft() ? right : (outerw - right - width), top, width, lines, align, yFrom, yTo, removeFromEnd, breakEverywhere, selection);
3077 }
3078 
getState(QPoint point,int width,StateRequest request) const3079 StateResult String::getState(QPoint point, int width, StateRequest request) const {
3080 	return Renderer(nullptr, this).getState(point, width, request);
3081 }
3082 
getStateLeft(QPoint point,int width,int outerw,StateRequest request) const3083 StateResult String::getStateLeft(QPoint point, int width, int outerw, StateRequest request) const {
3084 	return getState(style::rtlpoint(point, outerw), width, request);
3085 }
3086 
getStateElided(QPoint point,int width,StateRequestElided request) const3087 StateResult String::getStateElided(QPoint point, int width, StateRequestElided request) const {
3088 	return Renderer(nullptr, this).getStateElided(point, width, request);
3089 }
3090 
getStateElidedLeft(QPoint point,int width,int outerw,StateRequestElided request) const3091 StateResult String::getStateElidedLeft(QPoint point, int width, int outerw, StateRequestElided request) const {
3092 	return getStateElided(style::rtlpoint(point, outerw), width, request);
3093 }
3094 
adjustSelection(TextSelection selection,TextSelectType selectType) const3095 TextSelection String::adjustSelection(TextSelection selection, TextSelectType selectType) const {
3096 	uint16 from = selection.from, to = selection.to;
3097 	if (from < _text.size() && from <= to) {
3098 		if (to > _text.size()) to = _text.size();
3099 		if (selectType == TextSelectType::Paragraphs) {
3100 			if (!IsParagraphSeparator(_text.at(from))) {
3101 				while (from > 0 && !IsParagraphSeparator(_text.at(from - 1))) {
3102 					--from;
3103 				}
3104 			}
3105 			if (to < _text.size()) {
3106 				if (IsParagraphSeparator(_text.at(to))) {
3107 					++to;
3108 				} else {
3109 					while (to < _text.size() && !IsParagraphSeparator(_text.at(to))) {
3110 						++to;
3111 					}
3112 				}
3113 			}
3114 		} else if (selectType == TextSelectType::Words) {
3115 			if (!IsWordSeparator(_text.at(from))) {
3116 				while (from > 0 && !IsWordSeparator(_text.at(from - 1))) {
3117 					--from;
3118 				}
3119 			}
3120 			if (to < _text.size()) {
3121 				if (IsWordSeparator(_text.at(to))) {
3122 					++to;
3123 				} else {
3124 					while (to < _text.size() && !IsWordSeparator(_text.at(to))) {
3125 						++to;
3126 					}
3127 				}
3128 			}
3129 		}
3130 	}
3131 	return { from, to };
3132 }
3133 
isEmpty() const3134 bool String::isEmpty() const {
3135 	return _blocks.empty() || _blocks[0]->type() == TextBlockTSkip;
3136 }
3137 
countBlockEnd(const TextBlocks::const_iterator & i,const TextBlocks::const_iterator & e) const3138 uint16 String::countBlockEnd(const TextBlocks::const_iterator &i, const TextBlocks::const_iterator &e) const {
3139 	return (i + 1 == e) ? _text.size() : (*(i + 1))->from();
3140 }
3141 
countBlockLength(const String::TextBlocks::const_iterator & i,const String::TextBlocks::const_iterator & e) const3142 uint16 String::countBlockLength(const String::TextBlocks::const_iterator &i, const String::TextBlocks::const_iterator &e) const {
3143 	return countBlockEnd(i, e) - (*i)->from();
3144 }
3145 
3146 template <typename AppendPartCallback, typename ClickHandlerStartCallback, typename ClickHandlerFinishCallback, typename FlagsChangeCallback>
enumerateText(TextSelection selection,AppendPartCallback appendPartCallback,ClickHandlerStartCallback clickHandlerStartCallback,ClickHandlerFinishCallback clickHandlerFinishCallback,FlagsChangeCallback flagsChangeCallback) const3147 void String::enumerateText(TextSelection selection, AppendPartCallback appendPartCallback, ClickHandlerStartCallback clickHandlerStartCallback, ClickHandlerFinishCallback clickHandlerFinishCallback, FlagsChangeCallback flagsChangeCallback) const {
3148 	if (isEmpty() || selection.empty()) {
3149 		return;
3150 	}
3151 
3152 	int lnkIndex = 0;
3153 	uint16 lnkFrom = 0;
3154 	int32 flags = 0;
3155 	for (auto i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) {
3156 		int blockLnkIndex = (i == e) ? 0 : (*i)->lnkIndex();
3157 		uint16 blockFrom = (i == e) ? _text.size() : (*i)->from();
3158 		int32 blockFlags = (i == e) ? 0 : (*i)->flags();
3159 
3160 		if (blockLnkIndex && !_links.at(blockLnkIndex - 1)) { // ignore empty links
3161 			blockLnkIndex = 0;
3162 		}
3163 		if (blockLnkIndex != lnkIndex) {
3164 			if (lnkIndex) {
3165 				auto rangeFrom = qMax(selection.from, lnkFrom);
3166 				auto rangeTo = qMin(selection.to, blockFrom);
3167 				if (rangeTo > rangeFrom) { // handle click handler
3168 					const auto r = base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom);
3169 					if (lnkFrom != rangeFrom || blockFrom != rangeTo) {
3170 						// Ignore links that are partially copied.
3171 						clickHandlerFinishCallback(r, nullptr);
3172 					} else {
3173 						clickHandlerFinishCallback(r, _links.at(lnkIndex - 1));
3174 					}
3175 				}
3176 			}
3177 			lnkIndex = blockLnkIndex;
3178 			if (lnkIndex) {
3179 				lnkFrom = blockFrom;
3180 				clickHandlerStartCallback();
3181 			}
3182 		}
3183 
3184 		const auto checkBlockFlags = (blockFrom >= selection.from)
3185 			&& (blockFrom <= selection.to);
3186 		if (checkBlockFlags && blockFlags != flags) {
3187 			flagsChangeCallback(flags, blockFlags);
3188 			flags = blockFlags;
3189 		}
3190 		if (i == e || (lnkIndex ? lnkFrom : blockFrom) >= selection.to) {
3191 			break;
3192 		}
3193 
3194 		if ((*i)->type() == TextBlockTSkip) continue;
3195 
3196 		auto rangeFrom = qMax(selection.from, blockFrom);
3197 		auto rangeTo = qMin(selection.to, uint16(blockFrom + countBlockLength(i, e)));
3198 		if (rangeTo > rangeFrom) {
3199 			appendPartCallback(base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom));
3200 		}
3201 	}
3202 }
3203 
toString(TextSelection selection) const3204 QString String::toString(TextSelection selection) const {
3205 	return toText(selection, false, false).rich.text;
3206 }
3207 
toTextWithEntities(TextSelection selection) const3208 TextWithEntities String::toTextWithEntities(TextSelection selection) const {
3209 	return toText(selection, false, true).rich;
3210 }
3211 
toTextForMimeData(TextSelection selection) const3212 TextForMimeData String::toTextForMimeData(TextSelection selection) const {
3213 	return toText(selection, true, true);
3214 }
3215 
toText(TextSelection selection,bool composeExpanded,bool composeEntities) const3216 TextForMimeData String::toText(
3217 		TextSelection selection,
3218 		bool composeExpanded,
3219 		bool composeEntities) const {
3220 	struct MarkdownTagTracker {
3221 		TextBlockFlags flag = TextBlockFlags();
3222 		EntityType type = EntityType();
3223 		int start = 0;
3224 	};
3225 	auto result = TextForMimeData();
3226 	result.rich.text.reserve(_text.size());
3227 	if (composeExpanded) {
3228 		result.expanded.reserve(_text.size());
3229 	}
3230 	const auto insertEntity = [&](EntityInText &&entity) {
3231 		auto i = result.rich.entities.end();
3232 		while (i != result.rich.entities.begin()) {
3233 			auto j = i;
3234 			if ((--j)->offset() <= entity.offset()) {
3235 				break;
3236 			}
3237 			i = j;
3238 		}
3239 		result.rich.entities.insert(i, std::move(entity));
3240 	};
3241 	auto linkStart = 0;
3242 	auto markdownTrackers = composeEntities
3243 		? std::vector<MarkdownTagTracker>{
3244 			{ TextBlockFItalic, EntityType::Italic },
3245 			{ TextBlockFBold, EntityType::Bold },
3246 			{ TextBlockFSemibold, EntityType::Semibold },
3247 			{ TextBlockFUnderline, EntityType::Underline },
3248 			{ TextBlockFStrikeOut, EntityType::StrikeOut },
3249 			{ TextBlockFCode, EntityType::Code }, // #TODO entities
3250 			{ TextBlockFPre, EntityType::Pre }
3251 		} : std::vector<MarkdownTagTracker>();
3252 	const auto flagsChangeCallback = [&](int32 oldFlags, int32 newFlags) {
3253 		if (!composeEntities) {
3254 			return;
3255 		}
3256 		for (auto &tracker : markdownTrackers) {
3257 			const auto flag = tracker.flag;
3258 			if ((oldFlags & flag) && !(newFlags & flag)) {
3259 				insertEntity({
3260 					tracker.type,
3261 					tracker.start,
3262 					int(result.rich.text.size()) - tracker.start });
3263 			} else if ((newFlags & flag) && !(oldFlags & flag)) {
3264 				tracker.start = result.rich.text.size();
3265 			}
3266 		}
3267 	};
3268 	const auto clickHandlerStartCallback = [&] {
3269 		linkStart = result.rich.text.size();
3270 	};
3271 	const auto clickHandlerFinishCallback = [&](
3272 			QStringView inText,
3273 			const ClickHandlerPtr &handler) {
3274 		if (!handler || (!composeExpanded && !composeEntities)) {
3275 			return;
3276 		}
3277 		const auto entity = handler->getTextEntity();
3278 		const auto plainUrl = (entity.type == EntityType::Url)
3279 			|| (entity.type == EntityType::Email);
3280 		const auto full = plainUrl
3281 			? QStringView(entity.data).mid(0, entity.data.size())
3282 			: inText;
3283 		const auto customTextLink = (entity.type == EntityType::CustomUrl);
3284 		const auto internalLink = customTextLink
3285 			&& entity.data.startsWith(qstr("internal:"));
3286 		if (composeExpanded) {
3287 			const auto sameAsTextLink = customTextLink
3288 				&& (entity.data
3289 					== UrlClickHandler::EncodeForOpening(full.toString()));
3290 			if (customTextLink && !internalLink && !sameAsTextLink) {
3291 				const auto &url = entity.data;
3292 				result.expanded.append(qstr(" (")).append(url).append(')');
3293 			}
3294 		}
3295 		if (composeEntities && !internalLink) {
3296 			insertEntity({
3297 				entity.type,
3298 				linkStart,
3299 				int(result.rich.text.size() - linkStart),
3300 				plainUrl ? QString() : entity.data });
3301 		}
3302 	};
3303 	const auto appendPartCallback = [&](QStringView part) {
3304 		result.rich.text += part;
3305 		if (composeExpanded) {
3306 			result.expanded += part;
3307 		}
3308 	};
3309 
3310 	enumerateText(
3311 		selection,
3312 		appendPartCallback,
3313 		clickHandlerStartCallback,
3314 		clickHandlerFinishCallback,
3315 		flagsChangeCallback);
3316 
3317 	if (composeEntities) {
3318 		const auto proj = [](const EntityInText &entity) {
3319 			const auto type = entity.type();
3320 			const auto isUrl = (type == EntityType::Url)
3321 				|| (type == EntityType::CustomUrl)
3322 				|| (type == EntityType::BotCommand)
3323 				|| (type == EntityType::Mention)
3324 				|| (type == EntityType::MentionName)
3325 				|| (type == EntityType::Hashtag)
3326 				|| (type == EntityType::Cashtag);
3327 			return std::pair{ entity.offset(), isUrl ? 0 : 1 };
3328 		};
3329 		const auto pred = [&](const EntityInText &a, const EntityInText &b) {
3330 			return proj(a) < proj(b);
3331 		};
3332 		std::sort(
3333 			result.rich.entities.begin(),
3334 			result.rich.entities.end(),
3335 			pred);
3336 	}
3337 
3338 	return result;
3339 }
3340 
toIsolatedEmoji() const3341 IsolatedEmoji String::toIsolatedEmoji() const {
3342 	auto result = IsolatedEmoji();
3343 	const auto skip = (_blocks.empty()
3344 		|| _blocks.back()->type() != TextBlockTSkip) ? 0 : 1;
3345 	if (_blocks.size() > kIsolatedEmojiLimit + skip) {
3346 		return IsolatedEmoji();
3347 	}
3348 	auto index = 0;
3349 	for (const auto &block : _blocks) {
3350 		const auto type = block->type();
3351 		if (block->lnkIndex()) {
3352 			return IsolatedEmoji();
3353 		} else if (type == TextBlockTEmoji) {
3354 			result.items[index++] = block.unsafe<EmojiBlock>()._emoji;
3355 		} else if (type != TextBlockTSkip) {
3356 			return IsolatedEmoji();
3357 		}
3358 	}
3359 	return result;
3360 }
3361 
clear()3362 void String::clear() {
3363 	clearFields();
3364 	_text.clear();
3365 }
3366 
clearFields()3367 void String::clearFields() {
3368 	_blocks.clear();
3369 	_links.clear();
3370 	_maxWidth = _minHeight = 0;
3371 	_startDir = Qt::LayoutDirectionAuto;
3372 }
3373 
IsWordSeparator(QChar ch)3374 bool IsWordSeparator(QChar ch) {
3375 	switch (ch.unicode()) {
3376 	case QChar::Space:
3377 	case QChar::LineFeed:
3378 	case '.':
3379 	case ',':
3380 	case '?':
3381 	case '!':
3382 	case '@':
3383 	case '#':
3384 	case '$':
3385 	case ':':
3386 	case ';':
3387 	case '-':
3388 	case '<':
3389 	case '>':
3390 	case '[':
3391 	case ']':
3392 	case '(':
3393 	case ')':
3394 	case '{':
3395 	case '}':
3396 	case '=':
3397 	case '/':
3398 	case '+':
3399 	case '%':
3400 	case '&':
3401 	case '^':
3402 	case '*':
3403 	case '\'':
3404 	case '"':
3405 	case '`':
3406 	case '~':
3407 	case '|':
3408 		return true;
3409 	default:
3410 		break;
3411 	}
3412 	return false;
3413 }
3414 
IsAlmostLinkEnd(QChar ch)3415 bool IsAlmostLinkEnd(QChar ch) {
3416 	switch (ch.unicode()) {
3417 	case '?':
3418 	case ',':
3419 	case '.':
3420 	case '"':
3421 	case ':':
3422 	case '!':
3423 	case '\'':
3424 		return true;
3425 	default:
3426 		break;
3427 	}
3428 	return false;
3429 }
3430 
IsLinkEnd(QChar ch)3431 bool IsLinkEnd(QChar ch) {
3432 	return (ch == TextCommand)
3433 		|| IsBad(ch)
3434 		|| IsSpace(ch)
3435 		|| IsNewline(ch)
3436 		|| ch.isLowSurrogate()
3437 		|| ch.isHighSurrogate();
3438 }
3439 
IsNewline(QChar ch)3440 bool IsNewline(QChar ch) {
3441 	return (ch == QChar::LineFeed)
3442 		|| (ch == 156);
3443 }
3444 
IsSpace(QChar ch,bool rich)3445 bool IsSpace(QChar ch, bool rich) {
3446 	return ch.isSpace()
3447 		|| (ch < 32 && !(rich && ch == TextCommand))
3448 		|| (ch == QChar::ParagraphSeparator)
3449 		|| (ch == QChar::LineSeparator)
3450 		|| (ch == QChar::ObjectReplacementCharacter)
3451 		|| (ch == QChar::CarriageReturn)
3452 		|| (ch == QChar::Tabulation)
3453 		|| (ch == QChar(8203)/*Zero width space.*/);
3454 }
3455 
IsDiac(QChar ch)3456 bool IsDiac(QChar ch) { // diac and variation selectors
3457 	return (ch.category() == QChar::Mark_NonSpacing)
3458 		|| (ch == 1652)
3459 		|| (ch >= 64606 && ch <= 64611);
3460 }
3461 
IsReplacedBySpace(QChar ch)3462 bool IsReplacedBySpace(QChar ch) {
3463 	// \xe2\x80[\xa8 - \xac\xad] // 8232 - 8237
3464 	// QString from1 = QString::fromUtf8("\xe2\x80\xa8"), to1 = QString::fromUtf8("\xe2\x80\xad");
3465 	// \xcc[\xb3\xbf\x8a] // 819, 831, 778
3466 	// QString bad1 = QString::fromUtf8("\xcc\xb3"), bad2 = QString::fromUtf8("\xcc\xbf"), bad3 = QString::fromUtf8("\xcc\x8a");
3467 	// [\x00\x01\x02\x07\x08\x0b-\x1f] // '\t' = 0x09
3468 	return (/*code >= 0x00 && */ch <= 0x02)
3469 		|| (ch >= 0x07 && ch <= 0x09)
3470 		|| (ch >= 0x0b && ch <= 0x1f)
3471 		|| (ch == 819)
3472 		|| (ch == 831)
3473 		|| (ch == 778)
3474 		|| (ch >= 8232 && ch <= 8237);
3475 }
3476 
IsTrimmed(QChar ch,bool rich)3477 bool IsTrimmed(QChar ch, bool rich) {
3478 	return (!rich || ch != TextCommand)
3479 		&& (IsSpace(ch) || IsBad(ch));
3480 }
3481 
3482 } // namespace Text
3483 } // namespace Ui
3484