Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Checks: "-*,
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-avoid-do-while,
-readability-magic-numbers,
-performance-noexcept-move-constructor,
-misc-non-private-member-variables-in-classes,
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
- 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)
- Dev: Refactored `OnceFlag`. (#6237, #6316)
- Dev: Bumped clang-format requirement to 19. (#6236)
- Dev: Added optional improved text wrapping through private Qt APIs. (#6265)
- Dev: Factored out AUMID to `Version`. (#6321)
- Dev: Silenced some warnings when compiling with clang-cl. (#6331)
- Dev: Added some commands for forcing a relayout (and related things) in channel views. (#6342)
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ option(BUILD_SHARED_LIBS "" OFF)
option(CHATTERINO_LTO "Enable LTO for all targets" OFF)
option(CHATTERINO_PLUGINS "Enable ALPHA plugin support in Chatterino" ON)
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)
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)

option(CHATTERINO_UPDATER "Enable update checks" ON)
mark_as_advanced(CHATTERINO_UPDATER)
Expand Down
6 changes: 6 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,12 @@ if (CHATTERINO_PLUGINS)
target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua sol2::sol2)
endif()

if (CHATTERINO_ALLOW_PRIVATE_QT_API)
target_link_libraries(${LIBRARY_PROJECT} PUBLIC Qt${MAJOR_QT_VERSION}::GuiPrivate)
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
CHATTERINO_WITH_PRIVATE_QT_API)
endif()

if (BUILD_WITH_QTKEYCHAIN)
target_link_libraries(${LIBRARY_PROJECT}
PUBLIC
Expand Down
91 changes: 88 additions & 3 deletions src/messages/MessageElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
#include <QJsonObject>
#include <QJsonValue>

#ifdef CHATTERINO_WITH_PRIVATE_QT_API
# include <QtGui/private/qtextengine_p.h>
#endif

namespace chatterino {

using namespace literals;
Expand Down Expand Up @@ -621,13 +625,93 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
}
}

// we done goofed, we need to wrap the text
// We done goofed, we need to wrap the text.
// If we allow the use of private Qt APIs, we can use Qt's text
// engine to accurately calculate the width of the text. Otherwise,
// we have to fall back to using horizontalAdvance which has some
// corner cases when processing whole words (see #5944).
#ifdef CHATTERINO_WITH_PRIVATE_QT_API
auto font =
app->getFonts()->getFont(this->style_, container.getScale());

// This code is similar to the one from QTextEngine::elidedText in
// the mode Qt::ElideRight (because that's essentially what we're
// doing here): https://github.com/qt/qtbase/blob/560bf5a07720eaa8cc589f424743db8ed1f1d902/src/gui/text/qtextengine.cpp#L3145
// A difference is that, once we detected EOL, we start again.

// The start of the current line in `word`
qsizetype actualStart = 0;
// This is treated like a view (from `actualStart`) over the word.
// It's a QString because QStackTextEngine doesn't support
// QStringViews as arguments.
QString view = word;

// This is essentially a loop over every line of text.
do
{
QStackTextEngine engine(view, font);
engine.validate(); // initialize the internal state

int pos = 0;
int nextBreak = 0;
QFixed currentWidth = 0;
int to = static_cast<int>(view.size());
bool needsBreak = false;

// Find the next grapheme boundary (`nextBreak`) at which we
// need to break because the text wouldn't fit into the
// container anymore.
do
{
pos = nextBreak;

++nextBreak;
while (nextBreak < engine.layoutData->string.size() &&
!engine.attributes()[nextBreak].graphemeBoundary)
{
++nextBreak;
}

auto nextWidth =
currentWidth + engine.width(pos, nextBreak - pos);
if (!container.fitsInLine(nextWidth.toReal()))
{
needsBreak = true;
if (pos == 0)
{
// Make sure that we consume at least one glyph.
// So this element will overflow
currentWidth = nextWidth;
}
else
{
// We didn't consume the glyph, it's for the next line
nextBreak = pos;
}
break;
}
currentWidth = nextWidth;
} while (nextBreak < to);
// Now we either processed the whole text or we need to break
container.addElementNoLineBreak(getTextLayoutElement(
word.sliced(actualStart, nextBreak), currentWidth.toReal(),
!needsBreak && this->hasTrailingSpace()));
if (needsBreak)
{
container.breakLine();
}

actualStart += nextBreak;
// Update the view
view = QString::fromRawData(word.constData() + actualStart,
word.size() - actualStart);
assert(needsBreak || view.isEmpty());
} while (!view.isEmpty());
#else
auto textLength = word.length();
int wordStart = 0;
width = 0;

// QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1

for (int i = 0; i < textLength; i++)
{
auto isSurrogate = word.size() > i + 1 &&
Expand Down Expand Up @@ -663,6 +747,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
//add the final piece of wrapped text
container.addElementNoLineBreak(getTextLayoutElement(
word.mid(wordStart), width, this->hasTrailingSpace()));
#endif
}
}
}
Expand Down
Loading