From 1fe7144a0746353ea629cd91b5c0d04a8434159b Mon Sep 17 00:00:00 2001 From: Eskil Abrahamsen Blomfeldt Date: Wed, 24 Aug 2022 13:25:19 +0200 Subject: [PATCH] Introduce QGlyphRun::stringIndexes() This introduces a way to trace each entry in the glyph index array to a specific index in the original text passed to QTextLayout, as well as a convenience function to access the original string from the QGlyphRun. The index information is stored in the logClusters array internally in Qt, but it contains the inverse information: For each character in the output string, it contains an index into the glyph array. In order to get the string indexes for each glyph, which makes a lot more sense in the context of the QGlyphRun API, we need to do a little search to construct the data. To avoid adding unnecessary allocations, we make the new APIs opt-in. If you do not specify anything, you will only get the glyph indexes and glyph positions as before. However, you can now specify exactly which parts of the layout to extract using an optional flags parameter. This also adds a manual test which can be very handy to visualize QTextLayouts and how they are split into QGlyphRuns. Fixes: QTBUG-103932 Change-Id: Ie4288fff338b9482aba0aba29fc7e1e59fa60900 Reviewed-by: Lars Knoll --- src/gui/text/qglyphrun.cpp | 67 +++- src/gui/text/qglyphrun.h | 6 + src/gui/text/qglyphrun_p.h | 4 + src/gui/text/qtextlayout.cpp | 160 ++++++++- src/gui/text/qtextlayout.h | 28 +- tests/auto/gui/text/qglyphrun/CMakeLists.txt | 1 + tests/auto/gui/text/qglyphrun/Ligatures.otf | Bin 0 -> 24624 bytes .../auto/gui/text/qglyphrun/tst_qglyphrun.cpp | 310 ++++++++++++++++++ tests/manual/qglyphruns/CMakeLists.txt | 17 + tests/manual/qglyphruns/controller.cpp | 57 ++++ tests/manual/qglyphruns/controller.h | 29 ++ tests/manual/qglyphruns/controller.ui | 200 +++++++++++ tests/manual/qglyphruns/glyphruninspector.cpp | 48 +++ tests/manual/qglyphruns/glyphruninspector.h | 31 ++ tests/manual/qglyphruns/main.cpp | 18 + tests/manual/qglyphruns/singleglyphrun.cpp | 73 +++++ tests/manual/qglyphruns/singleglyphrun.h | 30 ++ tests/manual/qglyphruns/singleglyphrun.ui | 61 ++++ tests/manual/qglyphruns/view.cpp | 70 ++++ tests/manual/qglyphruns/view.h | 42 +++ 20 files changed, 1237 insertions(+), 15 deletions(-) create mode 100644 tests/auto/gui/text/qglyphrun/Ligatures.otf create mode 100644 tests/manual/qglyphruns/CMakeLists.txt create mode 100644 tests/manual/qglyphruns/controller.cpp create mode 100644 tests/manual/qglyphruns/controller.h create mode 100644 tests/manual/qglyphruns/controller.ui create mode 100644 tests/manual/qglyphruns/glyphruninspector.cpp create mode 100644 tests/manual/qglyphruns/glyphruninspector.h create mode 100644 tests/manual/qglyphruns/main.cpp create mode 100644 tests/manual/qglyphruns/singleglyphrun.cpp create mode 100644 tests/manual/qglyphruns/singleglyphrun.h create mode 100644 tests/manual/qglyphruns/singleglyphrun.ui create mode 100644 tests/manual/qglyphruns/view.cpp create mode 100644 tests/manual/qglyphruns/view.h diff --git a/src/gui/text/qglyphrun.cpp b/src/gui/text/qglyphrun.cpp index 371dd26637..389b7403fd 100644 --- a/src/gui/text/qglyphrun.cpp +++ b/src/gui/text/qglyphrun.cpp @@ -465,7 +465,72 @@ QRectF QGlyphRun::boundingRect() const */ bool QGlyphRun::isEmpty() const { - return d->glyphIndexDataSize == 0; + return d->glyphIndexDataSize == 0 + && d->glyphPositionDataSize == 0 + && d->stringIndexes.isEmpty() + && d->sourceString.isEmpty(); +} + +/*! + Returns the string indexes corresponding to each glyph index, if the glyph run has been + constructed from a string and string indexes have been requested from the layout. In this case, + the length of the returned vector will correspond to the length of glyphIndexes(). In other + cases, it will be empty. + + Since a single glyph may correspond to multiple characters in the source string, there may be + gaps in the list of string indexes. For instance, if the string "first" is processed by a font + which contains a ligature for the character pair "fi", then the five character string will + generate a glyph run consisting of only four glyphs. Then the glyph indexes may in this case be + (1, 2, 3, 4) (four arbitrary glyph indexes) whereas the string indexes would be (0, 2, 3, 4). + The glyphs are in the logical order of the string, thus it is implied that the first glyphs + spans characters 0 and 1 in this case. + + Inversely, a single character may also generate multiple glyphs, in which case there will be + duplicate entries in the list of string indexes. + + The string indexes correspond to the string, optionally available through sourceString(). + + \sa setStringIndexes(), sourceString(), QTextLayout::glyphRuns() +*/ +QList QGlyphRun::stringIndexes() const +{ + return d->stringIndexes; +} + +/*! + Sets the list of string indexes corresponding to the glyph indexes to \a stringIndexes + + See stringIndexes() for more details on the conventions of this list. + + \sa sourceString() + */ +void QGlyphRun::setStringIndexes(const QList &stringIndexes) +{ + detach(); + d->stringIndexes = stringIndexes; +} + +/*! + Returns the string corresponding to the glyph run, if the glyph run has been created from + a string and the string has been requested from the layout. + + \sa setSourceString(), stringIndexes(), QTextLayout::glyphRuns() + */ +QString QGlyphRun::sourceString() const +{ + return d->sourceString; +} + +/*! + Set the string corresponding to the glyph run to \a sourceString. If set, the indexes returned + by stringIndexes() should be indexes into this string. + + \sa sourceString(), stringIndexes() + */ +void QGlyphRun::setSourceString(const QString &sourceString) +{ + detach(); + d->sourceString = sourceString; } QT_END_NAMESPACE diff --git a/src/gui/text/qglyphrun.h b/src/gui/text/qglyphrun.h index 47c41d849d..a338a35bc1 100644 --- a/src/gui/text/qglyphrun.h +++ b/src/gui/text/qglyphrun.h @@ -74,6 +74,12 @@ public: void setBoundingRect(const QRectF &boundingRect); QRectF boundingRect() const; + QList stringIndexes() const; + void setStringIndexes(const QList &stringIndexes); + + void setSourceString(const QString &sourceString); + QString sourceString() const; + bool isEmpty() const; private: diff --git a/src/gui/text/qglyphrun_p.h b/src/gui/text/qglyphrun_p.h index 75bc832f77..c95ec8ab7f 100644 --- a/src/gui/text/qglyphrun_p.h +++ b/src/gui/text/qglyphrun_p.h @@ -42,8 +42,10 @@ public: : QSharedData(other) , glyphIndexes(other.glyphIndexes) , glyphPositions(other.glyphPositions) + , stringIndexes(other.stringIndexes) , rawFont(other.rawFont) , boundingRect(other.boundingRect) + , sourceString(other.sourceString) , flags(other.flags) , glyphIndexData(other.glyphIndexData) , glyphIndexDataSize(other.glyphIndexDataSize) @@ -56,8 +58,10 @@ public: QList glyphIndexes; QList glyphPositions; + QList stringIndexes; QRawFont rawFont; QRectF boundingRect; + QString sourceString; QGlyphRun::GlyphRunFlags flags; diff --git a/src/gui/text/qtextlayout.cpp b/src/gui/text/qtextlayout.cpp index 9cf5d8963b..43236eec91 100644 --- a/src/gui/text/qtextlayout.cpp +++ b/src/gui/text/qtextlayout.cpp @@ -281,6 +281,25 @@ Qt::LayoutDirection QTextInlineObject::textDirection() const \value SkipWords */ +/*! + \enum QTextLayout::GlyphRunRetrievalFlag + + GlyphRunRetrievalFlag specifies flags passed to the glyphRuns() functions to determine + which properties of the layout are returned in the QGlyphRun objects. Since each property + will consume memory and may require additional allocations, it is a good practice to only + request the properties you will need to access later. + + \value RetrieveGlyphIndexes Retrieves the indexes in the font which correspond to the glyphs. + \value RetrieveGlyphPositions Retrieves the relative positions of the glyphs in the layout. + \value RetrieveStringIndexes Retrieves the indexes in the original string that correspond to + each of the glyphs. + \value RetrieveString Retrieves the original source string from the layout. + \value RetrieveAll Retrieves all available properties of the layout. + \omitvalue DefaultRetrievalFlags + + \sa glyphRuns(), QTextLine::glyphRuns() +*/ + /*! \fn QTextEngine *QTextLayout::engine() const \internal @@ -961,7 +980,9 @@ static inline QRectF clipIfValid(const QRectF &rect, const QRectF &clip) } +#if QT_VERSION < QT_VERSION_CHECK(7, 0, 0) /*! + \overload Returns the glyph indexes and positions for all glyphs corresponding to the \a length characters starting at the position \a from in this QTextLayout. This is an expensive function, and should not be called in a time sensitive context. @@ -969,12 +990,44 @@ static inline QRectF clipIfValid(const QRectF &rect, const QRectF &clip) If \a from is less than zero, then the glyph run will begin at the first character in the layout. If \a length is less than zero, it will span the entire string from the start position. + \note This is equivalent to calling + glyphRuns(from, + length, + QTextLayout::GlyphRunRetrievalFlag::GlyphIndexes | + QTextLayout::GlyphRunRetrievalFlag::GlyphPositions). + \since 4.8 \sa draw(), QPainter::drawGlyphRun() */ -#if !defined(QT_NO_RAWFONT) +# if !defined(QT_NO_RAWFONT) QList QTextLayout::glyphRuns(int from, int length) const +{ + return glyphRuns(from, length, QTextLayout::GlyphRunRetrievalFlag::DefaultRetrievalFlags); +} +# endif +#endif + +/*! + \overload + Returns the glyph indexes and positions for all glyphs corresponding to the \a length characters + starting at the position \a from in this QTextLayout. This is an expensive function, and should + not be called in a time sensitive context. + + If \a from is less than zero, then the glyph run will begin at the first character in the + layout. If \a length is less than zero, it will span the entire string from the start position. + + The \a retrievalFlags specifies which properties of the QGlyphRun will be retrieved from the + layout. To minimize allocations and memory consumption, this should be set to include only the + properties that you need to access later. + + \since 6.5 + \sa draw(), QPainter::drawGlyphRun() +*/ +#if !defined(QT_NO_RAWFONT) +QList QTextLayout::glyphRuns(int from, + int length, + QTextLayout::GlyphRunRetrievalFlags retrievalFlags) const { if (from < 0) from = 0; @@ -986,10 +1039,11 @@ QList QTextLayout::glyphRuns(int from, int length) const if (d->lines.at(i).from > from + length) break; else if (d->lines.at(i).from + d->lines[i].length >= from) { - QList glyphRuns = QTextLine(i, d).glyphRuns(from, length); + QList glyphRuns = QTextLine(i, d).glyphRuns(from, length, retrievalFlags); for (int j = 0; j < glyphRuns.size(); j++) { const QGlyphRun &glyphRun = glyphRuns.at(j); + QRawFont rawFont = glyphRun.rawFont(); QFontEngine *fontEngine = rawFont.d->fontEngine; @@ -1002,14 +1056,17 @@ QList QTextLayout::glyphRuns(int from, int length) const } else { QList indexes = oldGlyphRun.glyphIndexes(); QList positions = oldGlyphRun.positions(); + QList stringIndexes = oldGlyphRun.stringIndexes(); QRectF boundingRect = oldGlyphRun.boundingRect(); indexes += glyphRun.glyphIndexes(); positions += glyphRun.positions(); + stringIndexes += glyphRun.stringIndexes(); boundingRect = boundingRect.united(glyphRun.boundingRect()); oldGlyphRun.setGlyphIndexes(indexes); oldGlyphRun.setPositions(positions); + oldGlyphRun.setStringIndexes(stringIndexes); oldGlyphRun.setBoundingRect(boundingRect); } } @@ -2186,9 +2243,11 @@ static void setPenAndDrawBackground(QPainter *p, const QPen &defaultPen, const Q #if !defined(QT_NO_RAWFONT) static QGlyphRun glyphRunWithInfo(QFontEngine *fontEngine, + const QString &text, const QGlyphLayout &glyphLayout, const QPointF &pos, const QGlyphRun::GlyphRunFlags &flags, + QTextLayout::GlyphRunRetrievalFlags retrievalFlags, QFixed selectionX, QFixed selectionWidth, int glyphsStart, @@ -2204,14 +2263,15 @@ static QGlyphRun glyphRunWithInfo(QFontEngine *fontEngine, QGlyphRunPrivate *d = QGlyphRunPrivate::get(glyphRun); int rangeStart = textPosition; - while (*logClusters != glyphsStart && rangeStart < textPosition + textLength) { - ++logClusters; + int logClusterIndex = 0; + while (logClusters[logClusterIndex] != glyphsStart && rangeStart < textPosition + textLength) { + ++logClusterIndex; ++rangeStart; } int rangeEnd = rangeStart; - while (*logClusters != glyphsEnd && rangeEnd < textPosition + textLength) { - ++logClusters; + while (logClusters[logClusterIndex] != glyphsEnd && rangeEnd < textPosition + textLength) { + ++logClusterIndex; ++rangeEnd; } @@ -2244,14 +2304,43 @@ static QGlyphRun glyphRunWithInfo(QFontEngine *fontEngine, qreal minY = 0; qreal maxY = 0; QList glyphs; - glyphs.reserve(glyphsArray.size()); + if (retrievalFlags & QTextLayout::RetrieveGlyphIndexes) + glyphs.reserve(glyphsArray.size()); QList positions; - positions.reserve(glyphsArray.size()); - for (int i=0; i stringIndexes; + if (retrievalFlags & QTextLayout::RetrieveStringIndexes) + stringIndexes.reserve(glyphsArray.size()); + + int nextClusterIndex = 0; + int currentClusterIndex = 0; + for (int i = 0; i < glyphsArray.size(); ++i) { + const int glyphArrayIndex = i + glyphsStart; + // Search for the next cluster in the string (or the end of string if there are no + // more clusters) + if (retrievalFlags & QTextLayout::RetrieveStringIndexes) { + if (nextClusterIndex < textLength && logClusters[nextClusterIndex] == glyphArrayIndex) { + currentClusterIndex = nextClusterIndex; // Store current cluster + while (logClusters[nextClusterIndex] == glyphArrayIndex && nextClusterIndex < textLength) + ++nextClusterIndex; + } + + // We are now either at end of string (no more clusters) or we are not yet at the + // next cluster in glyph array. We fill in current cluster so that there is always one + // entry in stringIndexes for each glyph. + Q_ASSERT(nextClusterIndex == textLength || logClusters[nextClusterIndex] != glyphArrayIndex); + stringIndexes.append(textPosition + currentClusterIndex); + } + + if (retrievalFlags & QTextLayout::RetrieveGlyphIndexes) { + glyph_t glyphIndex = glyphsArray.at(i) & 0xffffff; + glyphs.append(glyphIndex); + } QPointF position = positionsArray.at(i).toPointF() + pos; - positions.append(position); + if (retrievalFlags & QTextLayout::RetrieveGlyphPositions) + positions.append(position); if (i == 0) { maxY = minY = position.y(); @@ -2263,8 +2352,14 @@ static QGlyphRun glyphRunWithInfo(QFontEngine *fontEngine, qreal height = maxY + fontHeight - minY; - glyphRun.setGlyphIndexes(glyphs); - glyphRun.setPositions(positions); + if (retrievalFlags & QTextLayout::RetrieveGlyphIndexes) + glyphRun.setGlyphIndexes(glyphs); + if (retrievalFlags & QTextLayout::RetrieveGlyphPositions) + glyphRun.setPositions(positions); + if (retrievalFlags & QTextLayout::RetrieveStringIndexes) + glyphRun.setStringIndexes(stringIndexes); + if (retrievalFlags & QTextLayout::RetrieveString) + glyphRun.setSourceString(text); glyphRun.setFlags(flags); glyphRun.setRawFont(font); @@ -2274,7 +2369,9 @@ static QGlyphRun glyphRunWithInfo(QFontEngine *fontEngine, return glyphRun; } +# if QT_VERSION < QT_VERSION_CHECK(7, 0, 0) /*! + \overload Returns the glyph indexes and positions for all glyphs in this QTextLine for characters in the range defined by \a from and \a length. The \a from index is relative to the beginning of the text in the containing QTextLayout, and the range must be within the range of QTextLine @@ -2283,11 +2380,42 @@ static QGlyphRun glyphRunWithInfo(QFontEngine *fontEngine, If \a from is negative, it will default to textStart(), and if \a length is negative it will default to the return value of textLength(). + \note This is equivalent to calling + glyphRuns(from, + length, + QTextLayout::GlyphRunRetrievalFlag::GlyphIndexes | + QTextLayout::GlyphRunRetrievalFlag::GlyphPositions). + \since 5.0 \sa QTextLayout::glyphRuns() */ QList QTextLine::glyphRuns(int from, int length) const +{ + return glyphRuns(from, length, QTextLayout::GlyphRunRetrievalFlag::DefaultRetrievalFlags); +} +# endif + +/*! + Returns the glyph indexes and positions for all glyphs in this QTextLine for characters + in the range defined by \a from and \a length. The \a from index is relative to the beginning + of the text in the containing QTextLayout, and the range must be within the range of QTextLine + as given by functions textStart() and textLength(). + + The \a retrievalFlags specifies which properties of the QGlyphRun will be retrieved from the + layout. To minimize allocations and memory consumption, this should be set to include only the + properties that you need to access later. + + If \a from is negative, it will default to textStart(), and if \a length is negative it will + default to the return value of textLength(). + + \since 6.5 + + \sa QTextLayout::glyphRuns() +*/ +QList QTextLine::glyphRuns(int from, + int length, + QTextLayout::GlyphRunRetrievalFlags retrievalFlags) const { const QScriptLine &line = eng->lines.at(index); @@ -2405,9 +2533,11 @@ QList QTextLine::glyphRuns(int from, int length) const subFlags |= QGlyphRun::SplitLigature; glyphRuns.append(glyphRunWithInfo(multiFontEngine->engine(which), + eng->text, subLayout, pos, subFlags, + retrievalFlags, x, width, glyphsStart + start, @@ -2435,9 +2565,11 @@ QList QTextLine::glyphRuns(int from, int length) const subFlags |= QGlyphRun::SplitLigature; QGlyphRun glyphRun = glyphRunWithInfo(multiFontEngine->engine(which), + eng->text, subLayout, pos, subFlags, + retrievalFlags, x, width, glyphsStart + start, @@ -2451,9 +2583,11 @@ QList QTextLine::glyphRuns(int from, int length) const if (startsInsideLigature || endsInsideLigature) flags |= QGlyphRun::SplitLigature; QGlyphRun glyphRun = glyphRunWithInfo(mainFontEngine, + eng->text, glyphLayout, pos, flags, + retrievalFlags, x, width, glyphsStart, diff --git a/src/gui/text/qtextlayout.h b/src/gui/text/qtextlayout.h index 98054b8902..60ccb2ef27 100644 --- a/src/gui/text/qtextlayout.h +++ b/src/gui/text/qtextlayout.h @@ -69,6 +69,17 @@ class QTextOption; class Q_GUI_EXPORT QTextLayout { public: + enum GlyphRunRetrievalFlag { + RetrieveGlyphIndexes = 0x1, + RetrieveGlyphPositions = 0x2, + RetrieveStringIndexes = 0x4, + RetrieveString = 0x8, + + DefaultRetrievalFlags = RetrieveGlyphIndexes | RetrieveGlyphPositions, + RetrieveAll = 0xffff + }; + Q_DECLARE_FLAGS(GlyphRunRetrievalFlags, GlyphRunRetrievalFlag) + // does itemization QTextLayout(); QTextLayout(const QString& text); @@ -148,7 +159,15 @@ public: qreal maximumWidth() const; #if !defined(QT_NO_RAWFONT) + +# if QT_VERSION < QT_VERSION_CHECK(7, 0, 0) + QList glyphRuns(int from, int length, GlyphRunRetrievalFlags flags) const; QList glyphRuns(int from = -1, int length = -1) const; +# else + QList glyphRuns(int from = -1, + int length = -1, + GlyphRunRetrievalFlags flags = GlyphRunRetrievalFlag::DefaultRetrievalFlags) const; +# endif #endif QTextEngine *engine() const { return d; } @@ -166,7 +185,7 @@ private: QTextEngine *d; }; Q_DECLARE_TYPEINFO(QTextLayout::FormatRange, Q_RELOCATABLE_TYPE); - +Q_DECLARE_OPERATORS_FOR_FLAGS(QTextLayout::GlyphRunRetrievalFlags) class Q_GUI_EXPORT QTextLine { @@ -219,7 +238,14 @@ public: void draw(QPainter *painter, const QPointF &position) const; #if !defined(QT_NO_RAWFONT) +# if QT_VERSION < QT_VERSION_CHECK(7, 0, 0) + QList glyphRuns(int from, int length, QTextLayout::GlyphRunRetrievalFlags flags) const; QList glyphRuns(int from = -1, int length = -1) const; +# else + QList glyphRuns(int from = -1, + int length = -1, + QTextLayout::GlyphRunRetrievalFlags flags = QTextLayout::GlyphRunRetrievalFlag::Default) const; +# endif #endif private: diff --git a/tests/auto/gui/text/qglyphrun/CMakeLists.txt b/tests/auto/gui/text/qglyphrun/CMakeLists.txt index 4928b1c0d4..d6040ba625 100644 --- a/tests/auto/gui/text/qglyphrun/CMakeLists.txt +++ b/tests/auto/gui/text/qglyphrun/CMakeLists.txt @@ -13,6 +13,7 @@ set_source_files_properties("../../../shared/resources/test.ttf" ) set(testdata_resource_files "../../../shared/resources/test.ttf" + "Ligatures.otf" ) qt_internal_add_test(tst_qglyphrun diff --git a/tests/auto/gui/text/qglyphrun/Ligatures.otf b/tests/auto/gui/text/qglyphrun/Ligatures.otf new file mode 100644 index 0000000000000000000000000000000000000000..194218a0f646bfb20ded467b207393f5c8609934 GIT binary patch literal 24624 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 0); + + QFont ligatureFont = QFont("QtLigatures"); + QCOMPARE(QFontInfo(ligatureFont).family(), QString::fromLatin1("QtLigatures")); + + QTextLayout::GlyphRunRetrievalFlags retrievalFlags + = QTextLayout::RetrieveGlyphIndexes | QTextLayout::RetrieveStringIndexes; + + // Three characters -> three glyphs + { + QTextLayout layout; + layout.setText("f i"); + layout.setFont(ligatureFont); + layout.beginLayout(); + layout.createLine(); + layout.endLayout(); + + { + QList glyphRuns = layout.glyphRuns(-1, -1, retrievalFlags); + QCOMPARE(glyphRuns.size(), 1); + + QList stringIndexes = glyphRuns.at(0).stringIndexes(); + QCOMPARE(stringIndexes.size(), 3); + QCOMPARE(stringIndexes.at(0), 0); + QCOMPARE(stringIndexes.at(1), 1); + QCOMPARE(stringIndexes.at(2), 2); + } + + { + QList glyphRuns = layout.glyphRuns(2, -1, retrievalFlags); + QCOMPARE(glyphRuns.size(), 1); + + QList stringIndexes = glyphRuns.at(0).stringIndexes(); + QCOMPARE(stringIndexes.size(), 1); + QCOMPARE(stringIndexes.at(0), 2); + } + } + + // Two characters -> one glyph + { + QTextLayout layout; + layout.setText("fi"); + layout.setFont(ligatureFont); + layout.beginLayout(); + layout.createLine(); + layout.endLayout(); + + { + QList glyphRuns = layout.glyphRuns(-1, -1, retrievalFlags); + QCOMPARE(glyphRuns.size(), 1); + + QCOMPARE(glyphRuns.at(0).glyphIndexes().size(), 1); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(0), 233); + + QList stringIndexes = glyphRuns.at(0).stringIndexes(); + QCOMPARE(stringIndexes.size(), 1); + QCOMPARE(stringIndexes.at(0), 0); + } + + { + QList glyphRuns = layout.glyphRuns(1, -1, retrievalFlags); + QCOMPARE(glyphRuns.size(), 1); + + QCOMPARE(glyphRuns.at(0).glyphIndexes().size(), 1); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(0), 233); + + QList stringIndexes = glyphRuns.at(0).stringIndexes(); + QCOMPARE(stringIndexes.size(), 1); + QCOMPARE(stringIndexes.at(0), 1); + } + } + + // Four characters -> three glyphs + { + QTextLayout layout; + layout.setText("ffii"); + layout.setFont(ligatureFont); + layout.beginLayout(); + layout.createLine(); + layout.endLayout(); + + { + QList glyphRuns = layout.glyphRuns(-1, -1, retrievalFlags); + QCOMPARE(glyphRuns.size(), 1); + + QCOMPARE(glyphRuns.at(0).glyphIndexes().size(), 3); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(0), 71); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(1), 233); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(2), 74); + + QList stringIndexes = glyphRuns.at(0).stringIndexes(); + QCOMPARE(stringIndexes.size(), 3); + QCOMPARE(stringIndexes.at(0), 0); + QCOMPARE(stringIndexes.at(1), 1); + QCOMPARE(stringIndexes.at(2), 3); + } + + { + QList glyphRuns = layout.glyphRuns(1, 1, retrievalFlags); + QCOMPARE(glyphRuns.size(), 1); + + QCOMPARE(glyphRuns.at(0).glyphIndexes().size(), 1); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(0), 233); + + QList stringIndexes = glyphRuns.at(0).stringIndexes(); + QCOMPARE(stringIndexes.size(), 1); + QCOMPARE(stringIndexes.at(0), 1); + } + + { + QList glyphRuns = layout.glyphRuns(1, 2, retrievalFlags); + QCOMPARE(glyphRuns.size(), 1); + + QCOMPARE(glyphRuns.at(0).glyphIndexes().size(), 1); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(0), 233); + + QList stringIndexes = glyphRuns.at(0).stringIndexes(); + QCOMPARE(stringIndexes.size(), 1); + QCOMPARE(stringIndexes.at(0), 1); + } + + { + QList glyphRuns = layout.glyphRuns(1, 3, retrievalFlags); + QCOMPARE(glyphRuns.size(), 1); + + QCOMPARE(glyphRuns.at(0).glyphIndexes().size(), 2); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(0), 233); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(1), 74); + + QList stringIndexes = glyphRuns.at(0).stringIndexes(); + QCOMPARE(stringIndexes.size(), 2); + QCOMPARE(stringIndexes.at(0), 1); + QCOMPARE(stringIndexes.at(1), 3); + } + + } + + // One character -> two glyphs + { + QTextLayout layout; + layout.setText(QChar(0xe6)); // LATIN SMALL LETTER AE + layout.setFont(ligatureFont); + layout.beginLayout(); + layout.createLine(); + layout.endLayout(); + + QList glyphRuns = layout.glyphRuns(-1, -1, retrievalFlags); + QCOMPARE(glyphRuns.size(), 1); + + QCOMPARE(glyphRuns.at(0).glyphIndexes().size(), 2); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(0), 66); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(1), 70); + + QList stringIndexes = glyphRuns.at(0).stringIndexes(); + QCOMPARE(stringIndexes.size(), 2); + QCOMPARE(stringIndexes.at(0), 0); + QCOMPARE(stringIndexes.at(1), 0); + } + + // Three characters -> four glyphs + { + QTextLayout layout; + layout.setText(QString('f') + QChar(0xe6) + QChar('i')); + layout.setFont(ligatureFont); + layout.beginLayout(); + layout.createLine(); + layout.endLayout(); + + { + QList glyphRuns = layout.glyphRuns(-1, -1, retrievalFlags); + QCOMPARE(glyphRuns.size(), 1); + + QCOMPARE(glyphRuns.at(0).glyphIndexes().size(), 4); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(0), 71); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(1), 66); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(2), 70); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(3), 74); + + QList stringIndexes = glyphRuns.at(0).stringIndexes(); + QCOMPARE(stringIndexes.size(), 4); + QCOMPARE(stringIndexes.at(0), 0); + QCOMPARE(stringIndexes.at(1), 1); + QCOMPARE(stringIndexes.at(2), 1); + QCOMPARE(stringIndexes.at(3), 2); + } + + { + QList glyphRuns = layout.glyphRuns(1, -1, retrievalFlags); + QCOMPARE(glyphRuns.size(), 1); + + QCOMPARE(glyphRuns.at(0).glyphIndexes().size(), 3); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(0), 66); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(1), 70); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(2), 74); + + QList stringIndexes = glyphRuns.at(0).stringIndexes(); + QCOMPARE(stringIndexes.size(), 3); + QCOMPARE(stringIndexes.at(0), 1); + QCOMPARE(stringIndexes.at(1), 1); + QCOMPARE(stringIndexes.at(2), 2); + } + + { + QList glyphRuns = layout.glyphRuns(0, 2, retrievalFlags); + QCOMPARE(glyphRuns.size(), 1); + + QCOMPARE(glyphRuns.at(0).glyphIndexes().size(), 3); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(0), 71); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(1), 66); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(2), 70); + + QList stringIndexes = glyphRuns.at(0).stringIndexes(); + QCOMPARE(stringIndexes.size(), 3); + QCOMPARE(stringIndexes.at(0), 0); + QCOMPARE(stringIndexes.at(1), 1); + QCOMPARE(stringIndexes.at(2), 1); + } + + + } + + // Five characters -> five glyphs + { + QTextLayout layout; + layout.setText(QLatin1String("ffi") + QChar(0xe6) + QLatin1Char('i')); + layout.setFont(ligatureFont); + layout.beginLayout(); + layout.createLine(); + layout.endLayout(); + + QList glyphRuns = layout.glyphRuns(-1, -1, retrievalFlags); + QCOMPARE(glyphRuns.size(), 1); + + QCOMPARE(glyphRuns.at(0).glyphIndexes().size(), 5); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(0), 71); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(1), 233); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(2), 66); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(3), 70); + QCOMPARE(glyphRuns.at(0).glyphIndexes().at(4), 74); + + QList stringIndexes = glyphRuns.at(0).stringIndexes(); + QCOMPARE(stringIndexes.size(), 5); + QCOMPARE(stringIndexes.at(0), 0); + QCOMPARE(stringIndexes.at(1), 1); + QCOMPARE(stringIndexes.at(2), 3); + QCOMPARE(stringIndexes.at(3), 3); + QCOMPARE(stringIndexes.at(4), 4); + } + +} + +void tst_QGlyphRun::retrievalFlags_data() +{ + QTest::addColumn("flags"); + QTest::addColumn("expectedGlyphIndexes"); + QTest::addColumn("expectedStringIndexes"); + QTest::addColumn("expectedString"); + QTest::addColumn("expectedGlyphPositions"); + + QTest::newRow("Glyph indexes") + << QTextLayout::GlyphRunRetrievalFlags(QTextLayout::RetrieveGlyphIndexes) + << true << false << false << false; + QTest::newRow("Glyph Positions") + << QTextLayout::GlyphRunRetrievalFlags(QTextLayout::RetrieveGlyphPositions) + << false << false << false << true; + QTest::newRow("String indexes") + << QTextLayout::GlyphRunRetrievalFlags(QTextLayout::RetrieveStringIndexes) + << false << true << false << false; + QTest::newRow("String") + << QTextLayout::GlyphRunRetrievalFlags(QTextLayout::RetrieveString) + << false << false << true << false; + + QTest::newRow("Default") + << QTextLayout::GlyphRunRetrievalFlags(QTextLayout::DefaultRetrievalFlags) + << true << false << false << true; + QTest::newRow("All") + << QTextLayout::GlyphRunRetrievalFlags(QTextLayout::RetrieveAll) + << true << true << true << true; +} + +void tst_QGlyphRun::retrievalFlags() +{ + QFETCH(QTextLayout::GlyphRunRetrievalFlags, flags); + QFETCH(bool, expectedGlyphIndexes); + QFETCH(bool, expectedStringIndexes); + QFETCH(bool, expectedString); + QFETCH(bool, expectedGlyphPositions); + + QTextLayout layout; + layout.setText(QLatin1String("abc")); + layout.beginLayout(); + layout.createLine(); + layout.endLayout(); + + QList glyphRuns = layout.glyphRuns(-1, -1, flags); + QVERIFY(!glyphRuns.isEmpty()); + + QGlyphRun firstGlyphRun = glyphRuns.first(); + QCOMPARE(firstGlyphRun.glyphIndexes().isEmpty(), !expectedGlyphIndexes); + QCOMPARE(firstGlyphRun.stringIndexes().isEmpty(), !expectedStringIndexes); + QCOMPARE(firstGlyphRun.sourceString().isEmpty(), !expectedString); + QCOMPARE(firstGlyphRun.positions().isEmpty(), !expectedGlyphPositions); +} + #endif // QT_NO_RAWFONT QTEST_MAIN(tst_QGlyphRun) diff --git a/tests/manual/qglyphruns/CMakeLists.txt b/tests/manual/qglyphruns/CMakeLists.txt new file mode 100644 index 0000000000..4af9c3e241 --- /dev/null +++ b/tests/manual/qglyphruns/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_manual_test(qglyphrun + GUI + SOURCES + main.cpp + controller.h controller.cpp controller.ui + view.h view.cpp + glyphruninspector.h glyphruninspector.cpp + singleglyphrun.h singleglyphrun.cpp singleglyphrun.ui + LIBRARIES + Qt::Gui + Qt::Widgets + ENABLE_AUTOGEN_TOOLS + uic +) diff --git a/tests/manual/qglyphruns/controller.cpp b/tests/manual/qglyphruns/controller.cpp new file mode 100644 index 0000000000..d63c23de32 --- /dev/null +++ b/tests/manual/qglyphruns/controller.cpp @@ -0,0 +1,57 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "controller.h" +#include "ui_controller.h" + +Controller::Controller(QWidget *parent) : + QWidget(parent), + ui(new Ui::Controller) +{ + ui->setupUi(this); + + connect(ui->cbWrap, &QCheckBox::toggled, this, &Controller::updateViews); + connect(ui->gbRange, &QGroupBox::toggled, this, &Controller::updateViews); + connect(ui->teSourceString, &QPlainTextEdit::textChanged, this, &Controller::updateViews); + connect(ui->hsLineWidth, &QSlider::valueChanged, this, &Controller::updateViews); + connect(ui->inspector, &GlyphRunInspector::updateBounds, ui->view, &View::setVisualizedBounds); + connect(ui->sbFrom, &QSpinBox::valueChanged, this, &Controller::updateViews); + connect(ui->sbTo, &QSpinBox::valueChanged, this, &Controller::updateViews); + connect(ui->teSourceString, &QPlainTextEdit::selectionChanged, this, &Controller::updateRange); + connect(ui->fcbFont, &QFontComboBox::currentFontChanged, this, &Controller::updateViews); +} + +Controller::~Controller() +{ + delete ui; +} + +void Controller::updateRange() +{ + if (ui->gbRange->isChecked()) { + QTextCursor cursor = ui->teSourceString->textCursor(); + if (cursor.hasSelection()) { + ui->sbFrom->setValue(cursor.selectionStart()); + ui->sbTo->setValue(cursor.selectionEnd() - 1); + + updateViews(); + } + } +} + +void Controller::updateViews() +{ + QString s = ui->teSourceString->toPlainText(); + ui->sbFrom->setMaximum(s.length()); + ui->sbTo->setMaximum(s.length()); + + s.replace('\n', QChar::LineSeparator); + ui->view->updateLayout(s, + qreal(ui->hsLineWidth->value()) * ui->hsLineWidth->width() / 100.0f, + ui->cbWrap ? QTextOption::WrapAtWordBoundaryOrAnywhere : QTextOption::ManualWrap, + ui->fcbFont->currentFont()); + + int start = ui->gbRange->isChecked() ? ui->sbFrom->value() : -1; + int length = ui->gbRange->isChecked() ? ui->sbTo->value() - start + 1 : -1; + ui->inspector->updateLayout(ui->view->layout(), start, length); +} diff --git a/tests/manual/qglyphruns/controller.h b/tests/manual/qglyphruns/controller.h new file mode 100644 index 0000000000..9771ae135f --- /dev/null +++ b/tests/manual/qglyphruns/controller.h @@ -0,0 +1,29 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef CONTROLLER_H +#define CONTROLLER_H + +#include + +namespace Ui { +class Controller; +} + +class Controller : public QWidget +{ + Q_OBJECT + +public: + explicit Controller(QWidget *parent = nullptr); + ~Controller(); + +private slots: + void updateViews(); + void updateRange(); + +private: + Ui::Controller *ui; +}; + +#endif // CONTROLLER_H diff --git a/tests/manual/qglyphruns/controller.ui b/tests/manual/qglyphruns/controller.ui new file mode 100644 index 0000000000..903135a281 --- /dev/null +++ b/tests/manual/qglyphruns/controller.ui @@ -0,0 +1,200 @@ + + + Controller + + + + 0 + 0 + 549 + 419 + + + + Form + + + + + + + + Source string: + + + + + + + + + + + + + + Wrap + + + true + + + + + + + + + + + + Range + + + true + + + false + + + + + + From + + + + + + + + + + To + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Display + + + + + + + 0 + 0 + + + + + + + + 1 + + + 100 + + + 100 + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Quit + + + + + + + + + + View + QWidget +
view.h
+ 1 +
+ + GlyphRunInspector + QWidget +
glyphruninspector.h
+ 1 +
+
+ + + + pbQuit + clicked() + Controller + close() + + + 517 + 398 + + + 35 + -16 + + + + +
diff --git a/tests/manual/qglyphruns/glyphruninspector.cpp b/tests/manual/qglyphruns/glyphruninspector.cpp new file mode 100644 index 0000000000..13a8df0181 --- /dev/null +++ b/tests/manual/qglyphruns/glyphruninspector.cpp @@ -0,0 +1,48 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "glyphruninspector.h" +#include "singleglyphrun.h" + +#include +#include + +GlyphRunInspector::GlyphRunInspector(QWidget *parent) + : QWidget{parent} +{ + QHBoxLayout *layout = new QHBoxLayout(this); + + m_tabWidget = new QTabWidget; + layout->addWidget(m_tabWidget); + + connect(m_tabWidget, + &QTabWidget::currentChanged, + this, + &GlyphRunInspector::updateVisualizationForTab); +} + +void GlyphRunInspector::updateVisualizationForTab() +{ + int currentTab = m_tabWidget->currentIndex(); + SingleGlyphRun *gr = currentTab >= 0 && currentTab < m_content.size() ? m_content.at(currentTab) : nullptr; + if (gr != nullptr) + emit updateBounds(gr->bounds()); +} + +void GlyphRunInspector::updateLayout(QTextLayout *layout, int start, int length) +{ + QList glyphRuns = layout->glyphRuns(start, length, QTextLayout::RetrieveAll); + + m_tabWidget->clear(); + qDeleteAll(m_content); + m_content.clear(); + for (int i = 0; i < glyphRuns.size(); ++i) { + SingleGlyphRun *w = new SingleGlyphRun(m_tabWidget); + w->updateGlyphRun(glyphRuns.at(i)); + + m_tabWidget->addTab(w, QStringLiteral("%1").arg(glyphRuns.at(i).rawFont().familyName())); + m_content.append(w); + } + + updateVisualizationForTab(); +} diff --git a/tests/manual/qglyphruns/glyphruninspector.h b/tests/manual/qglyphruns/glyphruninspector.h new file mode 100644 index 0000000000..4b3943cf6a --- /dev/null +++ b/tests/manual/qglyphruns/glyphruninspector.h @@ -0,0 +1,31 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef GLYPHRUNINSPECTOR_H +#define GLYPHRUNINSPECTOR_H + +#include + +class QTextLayout; +class QTabWidget; +class SingleGlyphRun; +class GlyphRunInspector : public QWidget +{ + Q_OBJECT +public: + explicit GlyphRunInspector(QWidget *parent = nullptr); + + void updateLayout(QTextLayout *layout, int start, int length); + +private slots: + void updateVisualizationForTab(); + +signals: + void updateBounds(const QRegion ®ion); + +private: + QTabWidget *m_tabWidget; + QList m_content; +}; + +#endif // GLYPHRUNINSPECTOR_H diff --git a/tests/manual/qglyphruns/main.cpp b/tests/manual/qglyphruns/main.cpp new file mode 100644 index 0000000000..6f76b66d14 --- /dev/null +++ b/tests/manual/qglyphruns/main.cpp @@ -0,0 +1,18 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "controller.h" + +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + Controller controller; + controller.showMaximized(); + + return app.exec(); +} + +#include "main.moc" diff --git a/tests/manual/qglyphruns/singleglyphrun.cpp b/tests/manual/qglyphruns/singleglyphrun.cpp new file mode 100644 index 0000000000..2836cfadd9 --- /dev/null +++ b/tests/manual/qglyphruns/singleglyphrun.cpp @@ -0,0 +1,73 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "singleglyphrun.h" +#include "ui_singleglyphrun.h" + +SingleGlyphRun::SingleGlyphRun(QWidget *parent) : + QWidget(parent), + ui(new Ui::SingleGlyphRun) +{ + ui->setupUi(this); +} + +SingleGlyphRun::~SingleGlyphRun() +{ + delete ui; +} + +void SingleGlyphRun::updateGlyphRun(const QGlyphRun &glyphRun) +{ + m_bounds = QRegion(); + + QList glyphIndexes = glyphRun.glyphIndexes(); + QList stringIndexes = glyphRun.stringIndexes(); + QList glyphPositions = glyphRun.positions(); + + ui->twGlyphRun->clearContents(); + ui->twGlyphRun->setRowCount(glyphIndexes.size()); + + for (int i = 0; i < glyphIndexes.size(); ++i) { + { + QTableWidgetItem *glyphIndex = new QTableWidgetItem(QString::number(glyphIndexes.at(i))); + ui->twGlyphRun->setItem(i, 0, glyphIndex); + } + + { + QPointF position = glyphPositions.at(i); + QTableWidgetItem *glyphPosition = new QTableWidgetItem(QStringLiteral("(%1, %2)") + .arg(position.x()) + .arg(position.y())); + ui->twGlyphRun->setItem(i, 1, glyphPosition); + } + + { + QTableWidgetItem *stringIndex = new QTableWidgetItem(QString::number(stringIndexes.at(i))); + ui->twGlyphRun->setItem(i, 2, stringIndex); + } + + QChar c = glyphRun.sourceString().at(stringIndexes.at(i)); + + { + QTableWidgetItem *unicode = new QTableWidgetItem(QString::number(c.unicode(), 16)); + ui->twGlyphRun->setItem(i, 3, unicode); + } + + { + QTableWidgetItem *character = new QTableWidgetItem(c); + ui->twGlyphRun->setItem(i, 4, character); + } + + { + QImage image = glyphRun.rawFont().alphaMapForGlyph(glyphIndexes.at(i)); + + QTableWidgetItem *glyphImage = new QTableWidgetItem(QIcon(QPixmap::fromImage(image)), QString{}); + ui->twGlyphRun->setItem(i, 5, glyphImage); + } + + QRectF brect = glyphRun.rawFont().boundingRect(glyphIndexes.at(i)); + brect.adjust(glyphPositions.at(i).x(), glyphPositions.at(i).y(), + glyphPositions.at(i).x(), glyphPositions.at(i).y()); + m_bounds += brect.toAlignedRect(); + } +} diff --git a/tests/manual/qglyphruns/singleglyphrun.h b/tests/manual/qglyphruns/singleglyphrun.h new file mode 100644 index 0000000000..c400640335 --- /dev/null +++ b/tests/manual/qglyphruns/singleglyphrun.h @@ -0,0 +1,30 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef SINGLEGLYPHRUN_H +#define SINGLEGLYPHRUN_H + +#include +#include + +namespace Ui { +class SingleGlyphRun; +} + +class SingleGlyphRun : public QWidget +{ + Q_OBJECT + +public: + explicit SingleGlyphRun(QWidget *parent = nullptr); + ~SingleGlyphRun(); + + void updateGlyphRun(const QGlyphRun &glyphRun); + QRegion bounds() const { return m_bounds; } + +private: + Ui::SingleGlyphRun *ui; + QRegion m_bounds; +}; + +#endif // SINGLEGLYPHRUN_H diff --git a/tests/manual/qglyphruns/singleglyphrun.ui b/tests/manual/qglyphruns/singleglyphrun.ui new file mode 100644 index 0000000000..e2c8860de0 --- /dev/null +++ b/tests/manual/qglyphruns/singleglyphrun.ui @@ -0,0 +1,61 @@ + + + SingleGlyphRun + + + + 0 + 0 + 599 + 300 + + + + + 0 + 0 + + + + Form + + + + + + + Glyph index + + + + + Glyph position + + + + + String index + + + + + Unicode + + + + + Character + + + + + Font glyph + + + + + + + + + diff --git a/tests/manual/qglyphruns/view.cpp b/tests/manual/qglyphruns/view.cpp new file mode 100644 index 0000000000..95c05c608d --- /dev/null +++ b/tests/manual/qglyphruns/view.cpp @@ -0,0 +1,70 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "view.h" +#include +#include + +View::View(QWidget *parent) + : QWidget{parent} +{ + setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum)); +} + +View::~View() +{ + delete m_layout; +} + +void View::updateLayout(const QString &sourceString, + float width, + QTextOption::WrapMode mode, + const QFont &font) +{ + if (m_layout == nullptr) + m_layout = new QTextLayout; + + m_layout->setText(sourceString); + QTextOption option; + option.setWrapMode(mode); + m_layout->setTextOption(option); + m_layout->setFont(font); + m_layout->beginLayout(); + float y = 0.0f; + forever { + QTextLine line = m_layout->createLine(); + if (!line.isValid()) + break; + + line.setLineWidth(width); + line.setPosition(QPointF(0, y)); + y += line.height(); + } + m_layout->endLayout(); + + update(); + updateGeometry(); +} + +QSize View::sizeHint() const +{ + if (m_layout != nullptr) + return m_layout->boundingRect().size().toSize(); + else + return QSize(100, 100); +} + +void View::paintEvent(QPaintEvent *) +{ + QPainter p(this); + if (m_layout != nullptr) + m_layout->draw(&p, QPointF(0, 0)); + if (!m_bounds.isEmpty()) { + p.setPen(Qt::NoPen); + p.setBrush(Qt::yellow); + p.setOpacity(0.25); + for (const QRect &r : m_bounds) { + p.drawRect(r); + } + } +} diff --git a/tests/manual/qglyphruns/view.h b/tests/manual/qglyphruns/view.h new file mode 100644 index 0000000000..0eaaebe829 --- /dev/null +++ b/tests/manual/qglyphruns/view.h @@ -0,0 +1,42 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef VIEW_H +#define VIEW_H + +#include +#include + +class QTextLayout; +class View : public QWidget +{ + Q_OBJECT +public: + explicit View(QWidget *parent = nullptr); + ~View() override; + + void updateLayout(const QString &sourceString, + float width, + QTextOption::WrapMode mode, + const QFont &font); + + QSize sizeHint() const override; + + QTextLayout *layout() const { return m_layout; } + +public slots: + void setVisualizedBounds(const QRegion ®ion) + { + m_bounds = region; + update(); + } + +protected: + void paintEvent(QPaintEvent *e) override; + +private: + QTextLayout *m_layout = nullptr; + QRegion m_bounds; +}; + +#endif // VIEW_H