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