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-owning-memory,
|
||||||
-cppcoreguidelines-avoid-magic-numbers,
|
-cppcoreguidelines-avoid-magic-numbers,
|
||||||
-cppcoreguidelines-avoid-const-or-ref-data-members,
|
-cppcoreguidelines-avoid-const-or-ref-data-members,
|
||||||
|
-cppcoreguidelines-avoid-do-while,
|
||||||
-readability-magic-numbers,
|
-readability-magic-numbers,
|
||||||
-performance-noexcept-move-constructor,
|
-performance-noexcept-move-constructor,
|
||||||
-misc-non-private-member-variables-in-classes,
|
-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: 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: Refactored `OnceFlag`. (#6237, #6316)
|
||||||
- Dev: Bumped clang-format requirement to 19. (#6236)
|
- 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: Factored out AUMID to `Version`. (#6321)
|
||||||
- Dev: Silenced some warnings when compiling with clang-cl. (#6331)
|
- 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)
|
- 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_LTO "Enable LTO for all targets" OFF)
|
||||||
option(CHATTERINO_PLUGINS "Enable ALPHA plugin support in Chatterino" ON)
|
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_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)
|
option(CHATTERINO_UPDATER "Enable update checks" ON)
|
||||||
mark_as_advanced(CHATTERINO_UPDATER)
|
mark_as_advanced(CHATTERINO_UPDATER)
|
||||||
|
|||||||
@@ -872,6 +872,12 @@ if (CHATTERINO_PLUGINS)
|
|||||||
target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua sol2::sol2)
|
target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua sol2::sol2)
|
||||||
endif()
|
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)
|
if (BUILD_WITH_QTKEYCHAIN)
|
||||||
target_link_libraries(${LIBRARY_PROJECT}
|
target_link_libraries(${LIBRARY_PROJECT}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QJsonValue>
|
#include <QJsonValue>
|
||||||
|
|
||||||
|
#ifdef CHATTERINO_WITH_PRIVATE_QT_API
|
||||||
|
# include <QtGui/private/qtextengine_p.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
using namespace literals;
|
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();
|
auto textLength = word.length();
|
||||||
int wordStart = 0;
|
int wordStart = 0;
|
||||||
width = 0;
|
width = 0;
|
||||||
|
|
||||||
// QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1
|
|
||||||
|
|
||||||
for (int i = 0; i < textLength; i++)
|
for (int i = 0; i < textLength; i++)
|
||||||
{
|
{
|
||||||
auto isSurrogate = word.size() > i + 1 &&
|
auto isSurrogate = word.size() > i + 1 &&
|
||||||
@@ -663,6 +747,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
|
|||||||
//add the final piece of wrapped text
|
//add the final piece of wrapped text
|
||||||
container.addElementNoLineBreak(getTextLayoutElement(
|
container.addElementNoLineBreak(getTextLayoutElement(
|
||||||
word.mid(wordStart), width, this->hasTrailingSpace()));
|
word.mid(wordStart), width, this->hasTrailingSpace()));
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user