Skip to content

Commit 908b822

Browse files
committed
feat: add optional improved text wrapping (Chatterino#6265)
See the cmake flag `CHATTERINO_ALLOW_PRIVATE_QT_API` for details on how to test this.
1 parent 44a128f commit 908b822

5 files changed

Lines changed: 97 additions & 3 deletions

File tree

.clang-tidy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Checks: "-*,
1919
-cppcoreguidelines-owning-memory,
2020
-cppcoreguidelines-avoid-magic-numbers,
2121
-cppcoreguidelines-avoid-const-or-ref-data-members,
22+
-cppcoreguidelines-avoid-do-while,
2223
-readability-magic-numbers,
2324
-performance-noexcept-move-constructor,
2425
-misc-non-private-member-variables-in-classes,

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
- Dev: Emoji style / set is now stored lowercase (and matched case-insensitively). Changing emoji style from this point on and then running an old version might mean you will use the Twitter emoji style by default. (#6300)
8383
- Dev: Refactored `OnceFlag`. (#6237, #6316)
8484
- Dev: Bumped clang-format requirement to 19. (#6236)
85+
- Dev: Added optional improved text wrapping through private Qt APIs. (#6265)
8586
- Dev: Factored out AUMID to `Version`. (#6321)
8687
- Dev: Silenced some warnings when compiling with clang-cl. (#6331)
8788
- Dev: Added some commands for forcing a relayout (and related things) in channel views. (#6342)

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ option(BUILD_SHARED_LIBS "" OFF)
3030
option(CHATTERINO_LTO "Enable LTO for all targets" OFF)
3131
option(CHATTERINO_PLUGINS "Enable ALPHA plugin support in Chatterino" ON)
3232
option(CHATTERINO_USE_GDI_FONTENGINE "Use the legacy GDI fontengine instead of the new DirectWrite one on Windows (Qt 6.8.0 and later)" ON)
33+
option(CHATTERINO_ALLOW_PRIVATE_QT_API "Allow uses of Qt's private API - when enabling this, Chatterino must use the EXACT Qt version it was compiled against" OFF)
3334

3435
option(CHATTERINO_UPDATER "Enable update checks" ON)
3536
mark_as_advanced(CHATTERINO_UPDATER)

src/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,12 @@ if (CHATTERINO_PLUGINS)
874874
target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua sol2::sol2)
875875
endif()
876876

877+
if (CHATTERINO_ALLOW_PRIVATE_QT_API)
878+
target_link_libraries(${LIBRARY_PROJECT} PUBLIC Qt${MAJOR_QT_VERSION}::GuiPrivate)
879+
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
880+
CHATTERINO_WITH_PRIVATE_QT_API)
881+
endif()
882+
877883
if (BUILD_WITH_QTKEYCHAIN)
878884
target_link_libraries(${LIBRARY_PROJECT}
879885
PUBLIC

src/messages/MessageElement.cpp

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
#include <QJsonObject>
2121
#include <QJsonValue>
2222

23+
#ifdef CHATTERINO_WITH_PRIVATE_QT_API
24+
# include <QtGui/private/qtextengine_p.h>
25+
#endif
26+
2327
namespace chatterino {
2428

2529
using namespace literals;
@@ -621,13 +625,93 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
621625
}
622626
}
623627

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
625711
auto textLength = word.length();
626712
int wordStart = 0;
627713
width = 0;
628714

629-
// QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1
630-
631715
for (int i = 0; i < textLength; i++)
632716
{
633717
auto isSurrogate = word.size() > i + 1 &&
@@ -663,6 +747,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
663747
//add the final piece of wrapped text
664748
container.addElementNoLineBreak(getTextLayoutElement(
665749
word.mid(wordStart), width, this->hasTrailingSpace()));
750+
#endif
666751
}
667752
}
668753
}

0 commit comments

Comments
 (0)