feat: add optional improved text wrapping (#6265)
See the cmake flag `CHATTERINO_ALLOW_PRIVATE_QT_API` for details on how to test this.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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 &&
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user