diff options
Diffstat (limited to 'tests/auto/gui/text/qtextlayout')
-rw-r--r-- | tests/auto/gui/text/qtextlayout/CMakeLists.txt | 21 | ||||
-rw-r--r-- | tests/auto/gui/text/qtextlayout/qtextlayout.pro | 5 | ||||
-rw-r--r-- | tests/auto/gui/text/qtextlayout/tst_qtextlayout.cpp | 605 |
3 files changed, 548 insertions, 83 deletions
diff --git a/tests/auto/gui/text/qtextlayout/CMakeLists.txt b/tests/auto/gui/text/qtextlayout/CMakeLists.txt new file mode 100644 index 0000000000..655c0985a0 --- /dev/null +++ b/tests/auto/gui/text/qtextlayout/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qtextlayout Test: +##################################################################### + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qtextlayout LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +qt_internal_add_test(tst_qtextlayout + SOURCES + tst_qtextlayout.cpp + LIBRARIES + Qt::CorePrivate + Qt::Gui + Qt::GuiPrivate +) diff --git a/tests/auto/gui/text/qtextlayout/qtextlayout.pro b/tests/auto/gui/text/qtextlayout/qtextlayout.pro deleted file mode 100644 index 1e67626d23..0000000000 --- a/tests/auto/gui/text/qtextlayout/qtextlayout.pro +++ /dev/null @@ -1,5 +0,0 @@ -CONFIG += testcase -TARGET = tst_qtextlayout -QT += core-private gui-private testlib -HEADERS += -SOURCES += tst_qtextlayout.cpp diff --git a/tests/auto/gui/text/qtextlayout/tst_qtextlayout.cpp b/tests/auto/gui/text/qtextlayout/tst_qtextlayout.cpp index 9c477589f9..209f5a56e2 100644 --- a/tests/auto/gui/text/qtextlayout/tst_qtextlayout.cpp +++ b/tests/auto/gui/text/qtextlayout/tst_qtextlayout.cpp @@ -1,38 +1,14 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses /* !!!!!! Warning !!!!! Please don't save this file in emacs. It contains utf8 text sequences emacs will silently convert to a series of question marks. */ -#include <QtTest/QtTest> +#include <QTest> @@ -62,16 +38,21 @@ private slots: void lineBreaking(); #ifdef QT_BUILD_INTERNAL void simpleBoundingRect(); + void threeLineBoundingRect_data(); void threeLineBoundingRect(); void boundingRectWithLongLineAndNoWrap(); void forcedBreaks(); void breakAny(); void noWrap(); + void cursorToXForInlineObjects(); void cursorToXForSetColumns(); void cursorToXForTrailingSpaces_data(); void cursorToXForTrailingSpaces(); void cursorToXInvalidInput(); + void cursorToXForBidiBoundaries_data(); + void cursorToXForBidiBoundaries(); + void horizontalAlignment_data(); void horizontalAlignment(); void horizontalAlignmentMultiline_data(); @@ -85,6 +66,8 @@ private slots: #ifdef QT_BUILD_INTERNAL void xToCursorAtEndOfLine(); #endif + void xToCursorForBidiEnds_data(); + void xToCursorForBidiEnds(); void boundingRectTopLeft(); void graphemeBoundaryForSurrogatePairs(); void tabStops(); @@ -138,6 +121,13 @@ private slots: void noModificationOfInputString(); void superscriptCrash_qtbug53911(); void showLineAndParagraphSeparatorsCrash(); + void koreanWordWrap(); + void tooManyDirectionalCharctersCrash_qtbug77819(); + void softHyphens_data(); + void softHyphens(); + void min_maximumWidth_data(); + void min_maximumWidth(); + void negativeLineWidth(); private: QFont testFont; @@ -302,29 +292,60 @@ void tst_QTextLayout::simpleBoundingRect() QString hello("hello world"); - const int width = hello.length() * testFont.pixelSize(); + const int width = hello.size() * testFont.pixelSize(); QTextLayout layout(hello, testFont); layout.beginLayout(); QTextLine line = layout.createLine(); line.setLineWidth(width); - QCOMPARE(line.textLength(), hello.length()); + QCOMPARE(line.textLength(), hello.size()); QCOMPARE(layout.boundingRect(), QRectF(0, 0, width, QFontMetrics(testFont).height())); } +void tst_QTextLayout::threeLineBoundingRect_data() +{ + QTest::addColumn<QChar>("wordBoundary1"); + QTest::addColumn<QChar>("wordBoundary2"); + QTest::newRow("2x' '") << QChar(' ') << QChar(' '); + QTest::newRow("2x'\\n'") << QChar('\n') << QChar('\n'); + QTest::newRow("' ' + '\\n'") << QChar(' ') << QChar('\n'); + QTest::newRow("'\\n' + ' '") << QChar('\n') << QChar(' '); + QTest::newRow("2x'\\t'") << QChar('\t') << QChar('\t'); + QTest::newRow("2xsoft hyphen") << QChar(0xad) << QChar(0xad); + QTest::newRow("2x'-'") << QChar('-') << QChar('-'); + QTest::newRow("2x'/'") << QChar('/') << QChar('/'); + QTest::newRow("soft hyphen + ' '") << QChar(0xad) << QChar(' '); + QTest::newRow("soft hyphen + '\\n'") << QChar(0xad) << QChar('\n'); + QTest::newRow("soft hyphen + '-'") << QChar(0xad) << QChar('-'); + QTest::newRow("' ' + soft hyphen") << QChar(' ') << QChar(0xad); + QTest::newRow("'\\n' + soft hyphen") << QChar('\n') << QChar(0xad); + QTest::newRow("'-' + soft hyphen") << QChar('-') << QChar(0xad); +} + void tst_QTextLayout::threeLineBoundingRect() { /* stricter check. break text into three lines */ + QFETCH(QChar, wordBoundary1); + QFETCH(QChar, wordBoundary2); QString firstWord("hello"); - QString secondWord("world"); - QString thirdWord("test"); - QString text(firstWord + ' ' + secondWord + ' ' + thirdWord); - - const int firstLineWidth = firstWord.length() * testFont.pixelSize(); - const int secondLineWidth = secondWord.length() * testFont.pixelSize(); - const int thirdLineWidth = thirdWord.length() * testFont.pixelSize(); + QString secondWord("test"); + QString thirdWord("world"); + QString text(firstWord + wordBoundary1 + secondWord + wordBoundary2 + thirdWord); + + int firstLineWidth = firstWord.size() * testFont.pixelSize(); + int secondLineWidth = secondWord.size() * testFont.pixelSize(); + int thirdLineWidth = thirdWord.size() * testFont.pixelSize(); + // Trailing spaces do not count to line width: + if (!wordBoundary1.isSpace()) + firstLineWidth += testFont.pixelSize(); + if (!wordBoundary2.isSpace()) + secondLineWidth += testFont.pixelSize(); + // But trailing spaces do count to line length: + const int firstLineLength = firstWord.size() + 1; + const int secondLineLength = secondWord.size() + 1; + const int thirdLineLength = thirdWord.size(); const int longestLine = qMax(firstLineWidth, qMax(secondLineWidth, thirdLineWidth)); @@ -337,8 +358,7 @@ void tst_QTextLayout::threeLineBoundingRect() line.setLineWidth(firstLineWidth); line.setPosition(QPoint(0, y)); QCOMPARE(line.textStart(), pos); - // + 1 for trailing space - QCOMPARE(line.textLength(), firstWord.length() + 1); + QCOMPARE(line.textLength(), firstLineLength); QCOMPARE(qRound(line.naturalTextWidth()), firstLineWidth); pos += line.textLength(); @@ -347,9 +367,8 @@ void tst_QTextLayout::threeLineBoundingRect() line = layout.createLine(); line.setLineWidth(secondLineWidth); line.setPosition(QPoint(0, y)); - // + 1 for trailing space QCOMPARE(line.textStart(), pos); - QCOMPARE(line.textLength(), secondWord.length() + 1); + QCOMPARE(line.textLength(), secondLineLength); QCOMPARE(qRound(line.naturalTextWidth()), secondLineWidth); pos += line.textLength(); @@ -358,9 +377,8 @@ void tst_QTextLayout::threeLineBoundingRect() line = layout.createLine(); line.setLineWidth(secondLineWidth); line.setPosition(QPoint(0, y)); - // no trailing space here! QCOMPARE(line.textStart(), pos); - QCOMPARE(line.textLength(), thirdWord.length()); + QCOMPARE(line.textLength(), thirdLineLength); QCOMPARE(qRound(line.naturalTextWidth()), thirdLineWidth); y += qRound(line.ascent() + line.descent()); @@ -371,7 +389,7 @@ void tst_QTextLayout::boundingRectWithLongLineAndNoWrap() { QString longString("thisisaverylongstringthatcannotbewrappedatallitjustgoesonandonlikeonebigword"); - const int width = longString.length() * testFont.pixelSize() / 20; // very small widthx + const int width = longString.size() * testFont.pixelSize() / 20; // very small widthx QTextLayout layout(longString, testFont); layout.beginLayout(); @@ -496,18 +514,24 @@ void tst_QTextLayout::noWrap() void tst_QTextLayout::cursorToXForInlineObjects() { - QChar ch(QChar::ObjectReplacementCharacter); - QString text(ch); - QTextLayout layout(text, testFont); - layout.beginLayout(); + QString text = QStringLiteral("<html><body><img src=\"\" width=\"32\" height=\"32\" /></body></html>"); - QTextEngine *engine = layout.engine(); - const int item = engine->findItem(0); - engine->layoutData->items[item].width = 32; + QTextDocument document; + document.setHtml(text); + QCOMPARE(document.blockCount(), 1); - QTextLine line = layout.createLine(); - line.setLineWidth(0x10000); + // Trigger layout + { + QImage img(1, 1, QImage::Format_ARGB32_Premultiplied); + QPainter p(&img); + document.drawContents(&p); + } + + QTextLayout *layout = document.firstBlock().layout(); + QVERIFY(layout != nullptr); + QCOMPARE(layout->lineCount(), 1); + QTextLine line = layout->lineAt(0); QCOMPARE(line.cursorToX(0), qreal(0)); QCOMPARE(line.cursorToX(1), qreal(32)); } @@ -700,6 +724,76 @@ void tst_QTextLayout::cursorToXInvalidInput() QCOMPARE(cursorPos, 3); } +void tst_QTextLayout::cursorToXForBidiBoundaries_data() +{ + QTest::addColumn<Qt::LayoutDirection>("textDirection"); + QTest::addColumn<QString>("text"); + QTest::addColumn<int>("cursorPosition"); + QTest::addColumn<int>("runsToInclude"); + + QTest::addRow("LTR, abcشزذabc, 0") << Qt::LeftToRight << "abcشزذabc" + << 0 << 0; + QTest::addRow("RTL, abcشزذabc, 9") << Qt::RightToLeft << "abcشزذabc" + << 9 << 1; + QTest::addRow("LTR, abcشزذabc, 3") << Qt::LeftToRight << "abcشزذabc" + << 0 << 0; + QTest::addRow("RTL, abcشزذabc, 6") << Qt::RightToLeft << "abcشزذabc" + << 9 << 1; + + QTest::addRow("LTR, شزذabcشزذ, 0") << Qt::LeftToRight << "شزذabcشزذ" + << 0 << 1; + QTest::addRow("RTL, شزذabcشزذ, 9") << Qt::RightToLeft << "شزذabcشزذ" + << 9 << 0; + QTest::addRow("LTR, شزذabcشزذ, 3") << Qt::LeftToRight << "شزذabcشزذ" + << 3 << 1; + QTest::addRow("RTL, شزذabcشزذ, 3") << Qt::RightToLeft << "شزذabcشزذ" + << 3 << 2; + QTest::addRow("LTR, شزذabcشزذ, 6") << Qt::LeftToRight << "شزذabcشزذ" + << 6 << 2; + QTest::addRow("RTL, شزذabcشزذ, 6") << Qt::RightToLeft << "شزذabcشزذ" + << 6 << 1; +} + +void tst_QTextLayout::cursorToXForBidiBoundaries() +{ + QFETCH(Qt::LayoutDirection, textDirection); + QFETCH(QString, text); + QFETCH(int, cursorPosition); + QFETCH(int, runsToInclude); + + QTextOption option; + option.setTextDirection(textDirection); + + QTextLayout layout(text, testFont); + layout.setTextOption(option); + layout.beginLayout(); + + { + QTextLine line = layout.createLine(); + line.setLineWidth(0x10000); + } + layout.endLayout(); + + QTextLine line = layout.lineAt(0); + QList<QGlyphRun> glyphRuns = line.glyphRuns(-1, + -1, + QTextLayout::RetrieveStringIndexes + | QTextLayout::RetrieveGlyphIndexes); + QVERIFY(runsToInclude <= glyphRuns.size()); + + std::sort(glyphRuns.begin(), glyphRuns.end(), + [](const QGlyphRun &first, const QGlyphRun &second) { + return first.stringIndexes().first() < second.stringIndexes().first(); + }); + + qreal expectedX = 0.0; + for (int i = 0; i < runsToInclude; ++i) { + expectedX += glyphRuns.at(i).boundingRect().width(); + } + + QCOMPARE(line.cursorToX(cursorPosition), expectedX); +} + void tst_QTextLayout::horizontalAlignment_data() { qreal width = TESTFONT_SIZE * 4; @@ -992,7 +1086,7 @@ void tst_QTextLayout::defaultWordSeparators_data() QString separators(".,:;-<>[](){}=/+%&^*"); separators += QLatin1String("!?"); - for (int i = 0; i < separators.count(); ++i) { + for (int i = 0; i < separators.size(); ++i) { QTest::newRow(QString::number(i).toLatin1().data()) << QString::fromLatin1("abcd") + separators.at(i) + QString::fromLatin1("efgh") << 0 << 4; @@ -1072,7 +1166,7 @@ void tst_QTextLayout::xToCursorAtEndOfLine() QString text = "FirstLine SecondLine"; text.replace('\n', QChar::LineSeparator); - const qreal firstLineWidth = QString("FirstLine").length() * testFont.pixelSize(); + const qreal firstLineWidth = QString("FirstLine").size() * testFont.pixelSize(); QTextLayout layout(text, testFont); layout.setCacheEnabled(true); @@ -1092,6 +1186,60 @@ void tst_QTextLayout::xToCursorAtEndOfLine() } #endif + +void tst_QTextLayout::xToCursorForBidiEnds_data() +{ + QTest::addColumn<Qt::LayoutDirection>("textDirection"); + QTest::addColumn<QString>("text"); + QTest::addColumn<int>("leftPosition"); + QTest::addColumn<int>("rightPosition"); + + QTest::addRow("LTR, abcشزذ") << Qt::LeftToRight << "abcشزذ" + << 0 << 6; + QTest::addRow("RTL, abcشزذ") << Qt::RightToLeft << "abcشزذ" + << 6 << 0; + QTest::addRow("LTR, شزذabc") << Qt::LeftToRight << "شزذabc" + << 0 << 6; + QTest::addRow("RTL, شزذabc") << Qt::RightToLeft << "شزذabc" + << 6 << 0; + QTest::addRow("LTR, شزذ123") << Qt::LeftToRight << "شزذ123" + << 0 << 6; + QTest::addRow("RTL, شزذ123") << Qt::RightToLeft << "شزذ123" + << 6 << 0; + + QTest::addRow("LTR, abcشزذabc") << Qt::LeftToRight << "abcشزذabc" + << 0 << 9; + QTest::addRow("RTL, abcشزذabc") << Qt::RightToLeft << "abcشزذabc" + << 9 << 0; + QTest::addRow("LTR, شزذabcشزذ") << Qt::LeftToRight << "شزذabcشزذ" + << 0 << 9; + QTest::addRow("RTL, شزذabcشزذ") << Qt::RightToLeft << "شزذabcشزذ" + << 9 << 0; +} + +void tst_QTextLayout::xToCursorForBidiEnds() +{ + QFETCH(Qt::LayoutDirection, textDirection); + QFETCH(QString, text); + QFETCH(int, leftPosition); + QFETCH(int, rightPosition); + + QTextOption option; + option.setTextDirection(textDirection); + + QTextLayout layout(text, testFont); + layout.setTextOption(option); + layout.beginLayout(); + + QTextLine line = layout.createLine(); + line.setLineWidth(0x10000); + + QCOMPARE(line.xToCursor(0), leftPosition); + QCOMPARE(line.xToCursor(line.width()), rightPosition); + + layout.endLayout(); +} + void tst_QTextLayout::boundingRectTopLeft() { QString text = "FirstLine\nSecondLine"; @@ -1116,8 +1264,8 @@ void tst_QTextLayout::graphemeBoundaryForSurrogatePairs() { QString txt; txt.append(QLatin1Char('a')); - txt.append(0xd87e); - txt.append(0xdc25); + txt.append(QChar(0xd87e)); + txt.append(QChar(0xdc25)); txt.append(QLatin1Char('b')); QTextLayout layout(txt); QTextEngine *engine = layout.engine(); @@ -1161,7 +1309,7 @@ void tst_QTextLayout::integerOverflow() QVERIFY(line.isValid()); line.setLineWidth(INT_MAX); - QCOMPARE(line.textLength(), txt.length()); + QCOMPARE(line.textLength(), txt.size()); QVERIFY(!layout.createLine().isValid()); @@ -1650,8 +1798,8 @@ QT_END_NAMESPACE void tst_QTextLayout::testTabDPIScale() { class MyPaintDevice : public QPaintDevice { - QPaintEngine *paintEngine () const { return 0; } - int metric (QPaintDevice::PaintDeviceMetric metric) const { + QPaintEngine *paintEngine () const override { return 0; } + int metric (QPaintDevice::PaintDeviceMetric metric) const override { switch(metric) { case QPaintDevice::PdmWidth: case QPaintDevice::PdmHeight: @@ -1736,7 +1884,7 @@ void tst_QTextLayout::capitalization_allUpperCase() QTextEngine *engine = layout.engine(); engine->itemize(); - QCOMPARE(engine->layoutData->items.count(), 1); + QCOMPARE(engine->layoutData->items.size(), 1); QCOMPARE(engine->layoutData->items.at(0).analysis.flags, ushort(QScriptAnalysis::Uppercase)); } @@ -1756,7 +1904,7 @@ void tst_QTextLayout::capitalization_allUpperCase_newline() QTextEngine *engine = layout.engine(); engine->itemize(); - QCOMPARE(engine->layoutData->items.count(), 3); + QCOMPARE(engine->layoutData->items.size(), 3); QCOMPARE(engine->layoutData->items.at(0).analysis.flags, ushort(QScriptAnalysis::Uppercase)); QCOMPARE(engine->layoutData->items.at(1).analysis.flags, ushort(QScriptAnalysis::LineOrParagraphSeparator)); QCOMPARE(engine->layoutData->items.at(2).analysis.flags, ushort(QScriptAnalysis::Uppercase)); @@ -1774,7 +1922,7 @@ void tst_QTextLayout::capitalization_allLowerCase() QTextEngine *engine = layout.engine(); engine->itemize(); - QCOMPARE(engine->layoutData->items.count(), 1); + QCOMPARE(engine->layoutData->items.size(), 1); QCOMPARE(engine->layoutData->items.at(0).analysis.flags, ushort(QScriptAnalysis::Lowercase)); } @@ -1790,7 +1938,7 @@ void tst_QTextLayout::capitalization_smallCaps() QTextEngine *engine = layout.engine(); engine->itemize(); - QCOMPARE(engine->layoutData->items.count(), 2); + QCOMPARE(engine->layoutData->items.size(), 2); QCOMPARE(engine->layoutData->items.at(0).analysis.flags, ushort(QScriptAnalysis::None)); QCOMPARE(engine->layoutData->items.at(1).analysis.flags, ushort(QScriptAnalysis::SmallCaps)); } @@ -1807,7 +1955,7 @@ void tst_QTextLayout::capitalization_capitalize() QTextEngine *engine = layout.engine(); engine->itemize(); - QCOMPARE(engine->layoutData->items.count(), 5); + QCOMPARE(engine->layoutData->items.size(), 5); QCOMPARE(engine->layoutData->items.at(0).analysis.flags, ushort(QScriptAnalysis::Uppercase)); QCOMPARE(engine->layoutData->items.at(1).analysis.flags, ushort(QScriptAnalysis::None)); QCOMPARE(engine->layoutData->items.at(2).analysis.flags, ushort(QScriptAnalysis::Tab)); @@ -1862,6 +2010,34 @@ void tst_QTextLayout::longText() QVERIFY(line.isValid()); QVERIFY(line.cursorToX(line.textLength() - 1) > 0); } + + { + QTextLayout layout(QString("Qt rocks! ").repeated(200000)); + layout.setCacheEnabled(true); + layout.beginLayout(); + forever { + QTextLine line = layout.createLine(); + if (!line.isValid()) + break; + } + layout.endLayout(); + QFontMetricsF fm(layout.font()); + QVERIFY(layout.maximumWidth() - fm.horizontalAdvance(' ') <= QFIXED_MAX); + } + + { + QTextLayout layout(QString("AAAAAAAA").repeated(200000)); + layout.setCacheEnabled(true); + layout.beginLayout(); + forever { + QTextLine line = layout.createLine(); + if (!line.isValid()) + break; + } + layout.endLayout(); + QFontMetricsF fm(layout.font()); + QVERIFY(layout.maximumWidth() - fm.horizontalAdvance('A') <= QFIXED_MAX); + } } void tst_QTextLayout::widthOfTabs() @@ -1890,7 +2066,7 @@ void tst_QTextLayout::columnWrapWithTabs() textLayout.beginLayout(); QTextLine line = textLayout.createLine(); line.setNumColumns(30); - QCOMPARE(line.textLength(), text.length()); + QCOMPARE(line.textLength(), text.size()); textLayout.endLayout(); } @@ -1901,7 +2077,7 @@ void tst_QTextLayout::columnWrapWithTabs() textLayout.beginLayout(); QTextLine line = textLayout.createLine(); line.setNumColumns(30); - QVERIFY(line.textLength() < text.length()); + QVERIFY(line.textLength() < text.size()); textLayout.endLayout(); } @@ -1967,6 +2143,9 @@ void tst_QTextLayout::textWidthVsWIdth() layout.setCacheEnabled(true); QTextOption opt; opt.setWrapMode(QTextOption::WrapAnywhere); +#if defined(Q_OS_WIN) + layout.setFont(QFont(QString::fromLatin1("Arial"))); +#endif layout.setTextOption(opt); layout.setText(QString::fromLatin1( "g++ -c -m64 -pipe -g -fvisibility=hidden -fvisibility-inlines-hidden -Wall -W -D_REENTRANT -fPIC -DCORE_LIBRARY -DIDE_LIBRARY_BASENAME=\"lib\" -DWITH_TESTS " @@ -1983,6 +2162,21 @@ void tst_QTextLayout::textWidthVsWIdth() // minimum right bearing reported by the font engine doesn't cover all the glyphs in the font. // The result is that this test may fail in some cases. We should fix this by running the test // with a font that we know have no suprising right bearings. See qtextlayout.cpp for details. + QFontMetricsF fontMetrics(layout.font()); + QSet<char16_t> checked; + qreal minimumRightBearing = 0.0; + for (int i = 0; i < layout.text().size(); ++i) { + QChar c = layout.text().at(i); + if (!checked.contains(c.unicode())) { + qreal rightBearing = fontMetrics.rightBearing(c); + if (rightBearing < minimumRightBearing) + minimumRightBearing = rightBearing; + checked.insert(c.unicode()); + } + } + if (minimumRightBearing < fontMetrics.minRightBearing()) + QSKIP("Font reports invalid minimum right bearing, and can't be used for this test."); + for (int width = 100; width < 1000; ++width) { layout.beginLayout(); QTextLine line = layout.createLine(); @@ -2066,7 +2260,12 @@ void tst_QTextLayout::cursorInLigatureWithMultipleLines() void tst_QTextLayout::xToCursorForLigatures() { +#if defined(Q_OS_WIN32) + QTextLayout layout("fi", QFont("Calibri", 20)); +#else QTextLayout layout("fi", QFont("Times", 20)); +#endif + layout.setCacheEnabled(true); layout.beginLayout(); QTextLine line = layout.createLine(); @@ -2227,7 +2426,6 @@ void tst_QTextLayout::superscriptCrash_qtbug53911() for (int j = 0; j < 4; ++j) { QTextLayout* newTextLayout = new QTextLayout(); newTextLayout->setText(layoutText); - QList<QTextLayout::FormatRange> formatRanges; QTextLayout::FormatRange formatRange; formatRange.format.setFont(QFont()); @@ -2256,8 +2454,7 @@ void tst_QTextLayout::superscriptCrash_qtbug53911() formatRange.start = 0; formatRange.length = layoutText.size(); - formatRanges << formatRange; - newTextLayout->setAdditionalFormats(formatRanges); + newTextLayout->setFormats({formatRange}); textLayouts.push_front(newTextLayout); } @@ -2284,14 +2481,11 @@ void tst_QTextLayout::nbspWithFormat() layout.setText(s1 + s2 + nbsp + s3); QTextLayout::FormatRange formatRange; - formatRange.start = s1.length() + s2.length(); + formatRange.start = s1.size() + s2.size(); formatRange.length = 1; formatRange.format.setFontUnderline(true); - QList<QTextLayout::FormatRange> overrides; - overrides.append(formatRange); - - layout.setAdditionalFormats(overrides); + layout.setFormats({formatRange}); layout.beginLayout(); forever { @@ -2304,9 +2498,264 @@ void tst_QTextLayout::nbspWithFormat() QCOMPARE(layout.lineCount(), 2); QCOMPARE(layout.lineAt(0).textStart(), 0); - QCOMPARE(layout.lineAt(0).textLength(), s1.length()); - QCOMPARE(layout.lineAt(1).textStart(), s1.length()); - QCOMPARE(layout.lineAt(1).textLength(), s2.length() + 1 + s3.length()); + QCOMPARE(layout.lineAt(0).textLength(), s1.size()); + QCOMPARE(layout.lineAt(1).textStart(), s1.size()); + QCOMPARE(layout.lineAt(1).textLength(), s2.size() + 1 + s3.size()); +} + +void tst_QTextLayout::koreanWordWrap() +{ + QString s = QString::fromUtf8("안녕하세요 여러분!"); + QTextLayout layout; + QTextOption option = layout.textOption(); + option.setWrapMode(QTextOption::WordWrap); + option.setFlags(QTextOption::Flag(QTextOption::IncludeTrailingSpaces)); + layout.setTextOption(option); + layout.setText(s); + + QFontMetrics metrics(layout.font()); + + layout.beginLayout(); + forever { + QTextLine line = layout.createLine(); + if (!line.isValid()) + break; + line.setLineWidth(metrics.horizontalAdvance(s) * 0.8); + } + layout.endLayout(); + QCOMPARE(layout.lineCount(), 2); + QCOMPARE(layout.lineAt(0).textLength(), 6); + QCOMPARE(layout.lineAt(1).textLength(), 4); +} + +void tst_QTextLayout::tooManyDirectionalCharctersCrash_qtbug77819() +{ + QString data; + data += QString::fromUtf8("\xe2\x81\xa8"); // U+2068 FSI character + data += QString::fromUtf8("\xe2\x81\xa7"); // U+2067 RLI character + + // duplicating the text + for (int i = 0; i < 10; i++) + data += data; + + // Nothing to test. It must not crash in beginLayout(). + QTextLayout tl(data); + tl.beginLayout(); + tl.endLayout(); +} + +void tst_QTextLayout::softHyphens_data() +{ + QTest::addColumn<int>("fontSize"); + + QTest::newRow("12") << 12; + QTest::newRow("14") << 14; + QTest::newRow("16") << 16; +} + +void tst_QTextLayout::softHyphens() +{ + QFETCH(int, fontSize); + QString text = QStringLiteral("xxxx\u00ad") + QStringLiteral("xxxx\u00ad"); + + QFont font; + font.setPixelSize(fontSize); + font.setHintingPreference(QFont::PreferNoHinting); + const float xAdvance = QFontMetricsF(font).horizontalAdvance(QChar::fromLatin1('x')); + float shyWidth = 0.0f; + QTextLayout layout(text, font); + QTextOption option; + option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + layout.setTextOption(option); + { + // Calculate the effective width of a line-ending hyphen + // This calculation is currently done to work-around odditities on + // macOS 11 (see QTBUG-90698). + QTextLayout test(QStringLiteral("x\u00ad"), font); + // Note: This only works because Qt show the soft-hyphen when ending a text. + // This _could_ be considered a bug and the test would need to be changed + // if we stop doing that. + test.beginLayout(); + QTextLine line = test.createLine(); + line.setLineWidth(10 * xAdvance); + line.setPosition(QPoint(0, 0)); + shyWidth = line.naturalTextWidth() - xAdvance; + test.endLayout(); + } + qreal linefit; + // Loose fit + // xxxx- | + // xxxx- | + { + int pos = 0; + int y = 0; + layout.beginLayout(); + QTextLine line = layout.createLine(); + line.setLineWidth(qCeil(5 * xAdvance + shyWidth) + 1); + line.setPosition(QPoint(0, y)); + QCOMPARE(line.textStart(), pos); + QCOMPARE(line.textLength(), 5); + linefit = line.naturalTextWidth(); + QVERIFY(qAbs(linefit - qCeil(4 * xAdvance + shyWidth)) <= 1.0); + + pos += line.textLength(); + y += qRound(line.ascent() + line.descent()); + + line = layout.createLine(); + line.setLineWidth(qCeil(5 * xAdvance + shyWidth) + 1); + line.setPosition(QPoint(0, y)); + QCOMPARE(line.textStart(), pos); + QCOMPARE(line.textLength(), 5); + QVERIFY(qAbs(line.naturalTextWidth() - linefit) <= 1.0); + layout.endLayout(); + } + + // Tight fit + // xxxx-| + // xxxx-| + { + int pos = 0; + int y = 0; + layout.beginLayout(); + QTextLine line = layout.createLine(); + line.setLineWidth(qCeil(linefit) + 1); + line.setPosition(QPoint(0, y)); + QCOMPARE(line.textStart(), pos); + QCOMPARE(line.textLength(), 5); + QVERIFY(qAbs(line.naturalTextWidth() - linefit) <= 1.0); + + pos += line.textLength(); + y += qRound(line.ascent() + line.descent()); + + line = layout.createLine(); + line.setLineWidth(qCeil(linefit) + 1); + line.setPosition(QPoint(0, y)); + QCOMPARE(line.textStart(), pos); + QCOMPARE(line.textLength(), 5); + QVERIFY(qAbs(line.naturalTextWidth() - linefit) <= 1.0); + layout.endLayout(); + } + + // Very tight fit + // xxxx| + // xxxx| + // - | + { + int pos = 0; + int y = 0; + layout.beginLayout(); + QTextLine line = layout.createLine(); + line.setLineWidth(qCeil(4 * xAdvance) + 2); + line.setPosition(QPoint(0, y)); + QCOMPARE(line.textStart(), pos); + QCOMPARE(line.textLength(), 4); + QVERIFY(qAbs(line.naturalTextWidth() - qCeil(4 * xAdvance)) <= 1.0); + + pos += line.textLength(); + y += qRound(line.ascent() + line.descent()); + + line = layout.createLine(); + line.setLineWidth(qCeil(4 * xAdvance) + 2); + line.setPosition(QPoint(0, y)); + QCOMPARE(line.textStart(), pos); + QCOMPARE(line.textLength(), 5); + QVERIFY(qAbs(line.naturalTextWidth() - qCeil(4 * xAdvance)) <= 1.0); + + pos += line.textLength(); + y += qRound(line.ascent() + line.descent()); + + line = layout.createLine(); + line.setLineWidth(qCeil(4 * xAdvance) + 2); + line.setPosition(QPoint(0, y)); + QCOMPARE(line.textStart(), pos); + QCOMPARE(line.textLength(), 1); + QVERIFY(qAbs(line.naturalTextWidth() - shyWidth) <= 1.0); + layout.endLayout(); + } +} + +void tst_QTextLayout::min_maximumWidth_data() +{ + QTest::addColumn<QString>("text"); + + QTest::newRow("long string") << QStringLiteral("lmong_long_crazy_87235982735_23857239682376923876923876-fuwhfhfw-names-AAAA-deeaois2019-03-03.and.more"); + QTest::newRow("QTBUG-106947") << QStringLiteral("text text"); + QTest::newRow("spaces") << QStringLiteral(" text text "); + QTest::newRow("QTBUG-104986") << QStringLiteral("text\ntext\ntext"); + QTest::newRow("spaces + line breaks") << QStringLiteral(" \n text\n \ntext \n "); +} + +void tst_QTextLayout::min_maximumWidth() +{ + QFETCH(QString, text); + text.replace('\n', QChar::LineSeparator); + + QTextLayout layout(text, testFont); + layout.setCacheEnabled(true); + + QTextOption opt; + opt.setWrapMode(QTextOption::NoWrap); + layout.setTextOption(opt); + layout.beginLayout(); + while (layout.createLine().isValid()) { } + layout.endLayout(); + + const qreal nonWrappedMaxWidth = layout.maximumWidth(); + + for (int wrapMode = QTextOption::NoWrap; wrapMode <= QTextOption::WrapAtWordBoundaryOrAnywhere; ++wrapMode) { + opt.setWrapMode((QTextOption::WrapMode)wrapMode); + layout.setTextOption(opt); + layout.beginLayout(); + while (layout.createLine().isValid()) { } + layout.endLayout(); + const qreal minWidth = layout.minimumWidth(); + const qreal maxWidth = layout.maximumWidth(); + + QCOMPARE_LE(minWidth, maxWidth); + QCOMPARE_LE(maxWidth, nonWrappedMaxWidth); // maxWidth for wrapped text shouldn't exceed maxWidth for the text without wrapping. + + // Try the layout from slightly wider than the widest (maxWidth) + // and narrow it down to slighly narrower than minWidth + // layout.maximumWidth() should return the same regardless + qreal width = qCeil(maxWidth/10)*10 + 10; // begin a bit wider + const qreal stepSize = 20; + while (width >= minWidth - stepSize) { + layout.beginLayout(); + for (;;) { + QTextLine line = layout.createLine(); + if (!line.isValid()) + break; + line.setLineWidth(width); + } + layout.endLayout(); + QCOMPARE(layout.minimumWidth(), minWidth); + QCOMPARE(layout.maximumWidth(), maxWidth); + width -= stepSize; + } + } +} + +void tst_QTextLayout::negativeLineWidth() +{ + { + QTextLayout layout; + layout.setText("Foo bar"); + layout.beginLayout(); + QTextLine line = layout.createLine(); + line.setLineWidth(-1); + QVERIFY(line.textLength() > 0); + layout.endLayout(); + } + + { + QTextLayout layout; + layout.setText("Foo bar"); + layout.beginLayout(); + QTextLine line = layout.createLine(); + line.setNumColumns(2, -1); + QVERIFY(line.textLength() > 0); + layout.endLayout(); + } } QTEST_MAIN(tst_QTextLayout) |