|
20 | 20 | #include <QJsonObject> |
21 | 21 | #include <QJsonValue> |
22 | 22 |
|
| 23 | +#ifdef CHATTERINO_WITH_PRIVATE_QT_API |
| 24 | +# include <QtGui/private/qtextengine_p.h> |
| 25 | +#endif |
| 26 | + |
23 | 27 | namespace chatterino { |
24 | 28 |
|
25 | 29 | using namespace literals; |
@@ -621,13 +625,93 @@ void TextElement::addToContainer(MessageLayoutContainer &container, |
621 | 625 | } |
622 | 626 | } |
623 | 627 |
|
624 | | - // we done goofed, we need to wrap the text |
| 628 | + // We done goofed, we need to wrap the text. |
| 629 | + // If we allow the use of private Qt APIs, we can use Qt's text |
| 630 | + // engine to accurately calculate the width of the text. Otherwise, |
| 631 | + // we have to fall back to using horizontalAdvance which has some |
| 632 | + // corner cases when processing whole words (see #5944). |
| 633 | +#ifdef CHATTERINO_WITH_PRIVATE_QT_API |
| 634 | + auto font = |
| 635 | + app->getFonts()->getFont(this->style_, container.getScale()); |
| 636 | + |
| 637 | + // This code is similar to the one from QTextEngine::elidedText in |
| 638 | + // the mode Qt::ElideRight (because that's essentially what we're |
| 639 | + // doing here): https://github.com/qt/qtbase/blob/560bf5a07720eaa8cc589f424743db8ed1f1d902/src/gui/text/qtextengine.cpp#L3145 |
| 640 | + // A difference is that, once we detected EOL, we start again. |
| 641 | + |
| 642 | + // The start of the current line in `word` |
| 643 | + qsizetype actualStart = 0; |
| 644 | + // This is treated like a view (from `actualStart`) over the word. |
| 645 | + // It's a QString because QStackTextEngine doesn't support |
| 646 | + // QStringViews as arguments. |
| 647 | + QString view = word; |
| 648 | + |
| 649 | + // This is essentially a loop over every line of text. |
| 650 | + do |
| 651 | + { |
| 652 | + QStackTextEngine engine(view, font); |
| 653 | + engine.validate(); // initialize the internal state |
| 654 | + |
| 655 | + int pos = 0; |
| 656 | + int nextBreak = 0; |
| 657 | + QFixed currentWidth = 0; |
| 658 | + int to = static_cast<int>(view.size()); |
| 659 | + bool needsBreak = false; |
| 660 | + |
| 661 | + // Find the next grapheme boundary (`nextBreak`) at which we |
| 662 | + // need to break because the text wouldn't fit into the |
| 663 | + // container anymore. |
| 664 | + do |
| 665 | + { |
| 666 | + pos = nextBreak; |
| 667 | + |
| 668 | + ++nextBreak; |
| 669 | + while (nextBreak < engine.layoutData->string.size() && |
| 670 | + !engine.attributes()[nextBreak].graphemeBoundary) |
| 671 | + { |
| 672 | + ++nextBreak; |
| 673 | + } |
| 674 | + |
| 675 | + auto nextWidth = |
| 676 | + currentWidth + engine.width(pos, nextBreak - pos); |
| 677 | + if (!container.fitsInLine(nextWidth.toReal())) |
| 678 | + { |
| 679 | + needsBreak = true; |
| 680 | + if (pos == 0) |
| 681 | + { |
| 682 | + // Make sure that we consume at least one glyph. |
| 683 | + // So this element will overflow |
| 684 | + currentWidth = nextWidth; |
| 685 | + } |
| 686 | + else |
| 687 | + { |
| 688 | + // We didn't consume the glyph, it's for the next line |
| 689 | + nextBreak = pos; |
| 690 | + } |
| 691 | + break; |
| 692 | + } |
| 693 | + currentWidth = nextWidth; |
| 694 | + } while (nextBreak < to); |
| 695 | + // Now we either processed the whole text or we need to break |
| 696 | + container.addElementNoLineBreak(getTextLayoutElement( |
| 697 | + word.sliced(actualStart, nextBreak), currentWidth.toReal(), |
| 698 | + !needsBreak && this->hasTrailingSpace())); |
| 699 | + if (needsBreak) |
| 700 | + { |
| 701 | + container.breakLine(); |
| 702 | + } |
| 703 | + |
| 704 | + actualStart += nextBreak; |
| 705 | + // Update the view |
| 706 | + view = QString::fromRawData(word.constData() + actualStart, |
| 707 | + word.size() - actualStart); |
| 708 | + assert(needsBreak || view.isEmpty()); |
| 709 | + } while (!view.isEmpty()); |
| 710 | +#else |
625 | 711 | auto textLength = word.length(); |
626 | 712 | int wordStart = 0; |
627 | 713 | width = 0; |
628 | 714 |
|
629 | | - // QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1 |
630 | | - |
631 | 715 | for (int i = 0; i < textLength; i++) |
632 | 716 | { |
633 | 717 | auto isSurrogate = word.size() > i + 1 && |
@@ -663,6 +747,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container, |
663 | 747 | //add the final piece of wrapped text |
664 | 748 | container.addElementNoLineBreak(getTextLayoutElement( |
665 | 749 | word.mid(wordStart), width, this->hasTrailingSpace())); |
| 750 | +#endif |
666 | 751 | } |
667 | 752 | } |
668 | 753 | } |
|
0 commit comments