Skip to content

Commit be73ca5

Browse files
QTextLayout: Support inline objects for standalone layouts
Add support for ObjectReplacementCharacter as an inline object in standalone QTextLayout (without QTextDocument). When a QTextImageFormat is applied to an ObjectReplacementCharacter position via setFormats(), QTextEngine now recognizes it as an inline object and sizes it according to the format's width and height. In itemize(), ObjectReplacementCharacter is flagged as QScriptAnalysis::Object when a matching QTextImageFormat is found in specialData->formats. In shape(), the object dimensions are read from the image format and baseline metrics are computed to match what QQuickText expects. In layout_helper(), the ascent/descent update is now also applied for Object items in standalone layouts (no QTextDocument), since there is no special vertical alignment or floating to handle without a document. This enables QQuickText (StyledText) to use ObjectReplacementCharacter instead of Nbsp padding for inline images, giving the text layout engine accurate knowledge of image dimensions. This is a prerequisite for correct eliding, wrapping, and positioning of inline images. Task-number: QTBUG-112717 Task-number: QTBUG-97536 Task-number: QTBUG-57191 Task-number: QTBUG-36163 Task-number: QTBUG-39107 Task-number: QTBUG-43820 Task-number: QTBUG-113829 Task-number: QTBUG-133284 Change-Id: I49c3a81e4b6c35b4195af87dbcedeaaf1ff5e663 Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>
1 parent be64f61 commit be73ca5

3 files changed

Lines changed: 283 additions & 2 deletions

File tree

src/gui/text/qtextengine.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1879,6 +1879,40 @@ void QTextEngine::shape(int item) const
18791879
docLayout()->resizeInlineObject(QTextInlineObject(item, const_cast<QTextEngine *>(this)),
18801880
li.position + block.position(),
18811881
format(&li));
1882+
} else {
1883+
// Standalone QTextLayout (no QTextDocument): read the
1884+
// object size from a QTextImageFormat set via setFormats().
1885+
QTextCharFormat fmt = format(&li);
1886+
if (fmt.isImageFormat()) {
1887+
QTextImageFormat imgFmt = fmt.toImageFormat();
1888+
const qreal w = imgFmt.width();
1889+
const qreal h = imgFmt.height();
1890+
if (w > 0 && h > 0) {
1891+
QTextInlineObject obj(item, const_cast<QTextEngine *>(this));
1892+
obj.setWidth(w);
1893+
// Mirror QTextDocumentLayout::resizeInlineObject()
1894+
// vertical alignment logic.
1895+
const QFontMetricsF fm(fnt);
1896+
switch (fmt.verticalAlignment()) {
1897+
case QTextCharFormat::AlignMiddle: {
1898+
const qreal halfX = fm.xHeight() / 2.0;
1899+
obj.setAscent((h + halfX) / 2.0);
1900+
obj.setDescent((h - halfX) / 2.0);
1901+
break;
1902+
}
1903+
case QTextCharFormat::AlignBaseline: {
1904+
const qreal descent = fm.descent();
1905+
obj.setDescent(descent);
1906+
obj.setAscent(h - descent);
1907+
break;
1908+
}
1909+
default:
1910+
obj.setDescent(0);
1911+
obj.setAscent(h);
1912+
break;
1913+
}
1914+
}
1915+
}
18821916
}
18831917
// fix log clusters to point to the previous glyph, as the object doesn't have a glyph of it's own.
18841918
// This is required so that all entries in the array get initialized and are ordered correctly.
@@ -2100,6 +2134,18 @@ void QTextEngine::itemize() const
21002134
&& QAbstractTextDocumentLayoutPrivate::get(doc_p->layout()) != nullptr
21012135
&& QAbstractTextDocumentLayoutPrivate::get(doc_p->layout())->hasHandlers()) {
21022136
analysis->flags = QScriptAnalysis::Object;
2137+
} else if (specialData) {
2138+
// Standalone QTextLayout: check if a QTextImageFormat
2139+
// was set for this position via setFormats().
2140+
const int pos = uc - string;
2141+
analysis->flags = QScriptAnalysis::None;
2142+
for (const auto &range : std::as_const(specialData->formats)) {
2143+
if (range.start <= pos && pos < range.start + range.length
2144+
&& range.format.isImageFormat()) {
2145+
analysis->flags = QScriptAnalysis::Object;
2146+
break;
2147+
}
2148+
}
21032149
} else {
21042150
analysis->flags = QScriptAnalysis::None;
21052151
}

src/gui/text/qtextlayout.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1906,8 +1906,12 @@ void QTextLine::layout_helper(int maxGlyphs)
19061906
lbh.tmpData.leading = qMax(lbh.tmpData.leading + lbh.tmpData.ascent,
19071907
current.leading + current.ascent) - qMax(lbh.tmpData.ascent,
19081908
current.ascent);
1909-
if (current.analysis.flags != QScriptAnalysis::Object) {
1910-
// objects need some special treatment as they can special alignment or be floating
1909+
if (current.analysis.flags != QScriptAnalysis::Object
1910+
|| QTextDocumentPrivate::get(eng->block) == nullptr) {
1911+
// Objects with a QTextDocument may need special vertical alignment
1912+
// or floating treatment handled later, so skip the ascent/descent
1913+
// update for them here. Standalone objects (no document) are
1914+
// always included because no such post-processing exists.
19111915
lbh.tmpData.ascent = qMax(lbh.tmpData.ascent, current.ascent);
19121916
lbh.tmpData.descent = qMax(lbh.tmpData.descent, current.descent);
19131917
}

tests/auto/gui/text/qtextlayout/tst_qtextlayout.cpp

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ private slots:
129129
void embeddedImageLineHeight();
130130
void unmatchedShapedSubstring();
131131
void maximumLayoutWidthInWrappedLayout();
132+
void standaloneInlineObject_lineHeight();
133+
void standaloneInlineObject_verticalAlignment();
134+
#ifdef QT_BUILD_INTERNAL
135+
void standaloneInlineObject_cursorToX_data();
136+
void standaloneInlineObject_cursorToX();
137+
void standaloneInlineObject_wrapping();
138+
void standaloneInlineObject_eliding();
139+
#endif
132140

133141
private:
134142
QFont testFont;
@@ -2873,5 +2881,228 @@ void tst_QTextLayout::maximumLayoutWidthInWrappedLayout()
28732881
QCOMPARE(reference.maximumWidth(), breakByWidth.maximumWidth());
28742882
}
28752883

2884+
// Helper: build a standalone QTextLayout with ObjectReplacementCharacter
2885+
// at each position listed in |objectPositions|, sized via QTextImageFormat.
2886+
2887+
static constexpr qreal TEST_OBJECT_WIDTH = 32;
2888+
2889+
static void setupStandaloneLayoutWithObjects(QTextLayout &layout, const QString &text,
2890+
const QFont &font, const QList<int> &objectPositions,
2891+
qreal objWidth, qreal objHeight)
2892+
{
2893+
layout.setText(text);
2894+
layout.setFont(font);
2895+
2896+
QList<QTextLayout::FormatRange> formats;
2897+
for (int pos : objectPositions) {
2898+
Q_ASSERT(text.at(pos) == QChar::ObjectReplacementCharacter);
2899+
QTextLayout::FormatRange range;
2900+
QTextImageFormat imgFmt;
2901+
imgFmt.setWidth(objWidth);
2902+
imgFmt.setHeight(objHeight);
2903+
range.format = imgFmt;
2904+
range.start = pos;
2905+
range.length = 1;
2906+
formats.append(range);
2907+
}
2908+
layout.setFormats(formats);
2909+
}
2910+
2911+
void tst_QTextLayout::standaloneInlineObject_lineHeight()
2912+
{
2913+
// Not data-driven because expected values depend on testFont metrics
2914+
// which are only available after initTestCase() loads the test font.
2915+
const QChar orc = QChar::ObjectReplacementCharacter;
2916+
QString text = QStringLiteral("X") + orc + QStringLiteral("Y");
2917+
2918+
// Reference: plain text line metrics (no inline object).
2919+
QTextLayout refLayout(QStringLiteral("XY"), testFont);
2920+
refLayout.beginLayout();
2921+
QTextLine refLine = refLayout.createLine();
2922+
refLine.setLineWidth(1000);
2923+
refLayout.endLayout();
2924+
2925+
const qreal fontHeight = refLine.height();
2926+
const qreal fontDescent = refLine.descent();
2927+
2928+
auto testCase = [&](qreal objHeight, qreal expectedHeight) {
2929+
QTextLayout layout;
2930+
setupStandaloneLayoutWithObjects(layout, text, testFont, { 1 }, 20, objHeight);
2931+
layout.beginLayout();
2932+
QTextLine line = layout.createLine();
2933+
line.setLineWidth(1000);
2934+
layout.endLayout();
2935+
2936+
QCOMPARE(line.height(), expectedHeight);
2937+
QCOMPARE(line.descent(), fontDescent);
2938+
};
2939+
2940+
// Large object (taller than font) — line height equals object height.
2941+
testCase(80, 80);
2942+
// Font-sized object — line height equals font height.
2943+
testCase(TESTFONT_SIZE, fontHeight);
2944+
// Small object (shorter than font) — line height equals font height.
2945+
testCase(TESTFONT_SIZE / 4, fontHeight);
2946+
}
2947+
2948+
void tst_QTextLayout::standaloneInlineObject_verticalAlignment()
2949+
{
2950+
// Verify that the three vertical alignment modes from
2951+
// QTextDocumentLayout::resizeInlineObject() are supported.
2952+
const QChar orc = QChar::ObjectReplacementCharacter;
2953+
const qreal objHeight = 80;
2954+
const qreal objWidth = 20;
2955+
QString text = QStringLiteral("X") + orc + QStringLiteral("Y");
2956+
2957+
auto layoutWithAlignment = [&](QTextCharFormat::VerticalAlignment align) {
2958+
QTextLayout layout;
2959+
layout.setText(text);
2960+
layout.setFont(testFont);
2961+
2962+
QTextLayout::FormatRange range;
2963+
QTextImageFormat imgFmt;
2964+
imgFmt.setWidth(objWidth);
2965+
imgFmt.setHeight(objHeight);
2966+
imgFmt.setVerticalAlignment(align);
2967+
range.format = imgFmt;
2968+
range.start = 1;
2969+
range.length = 1;
2970+
layout.setFormats({ range });
2971+
2972+
layout.beginLayout();
2973+
QTextLine line = layout.createLine();
2974+
line.setLineWidth(1000);
2975+
layout.endLayout();
2976+
return std::make_pair(line.ascent(), line.descent());
2977+
};
2978+
2979+
// Default: descent=0, ascent=h → baseline at bottom of image.
2980+
{
2981+
auto [ascent, descent] = layoutWithAlignment(QTextCharFormat::AlignNormal);
2982+
QCOMPARE(ascent, objHeight);
2983+
QCOMPARE(descent, qreal(0));
2984+
}
2985+
2986+
// AlignBaseline: descent=fm.descent(), ascent=h-descent.
2987+
{
2988+
const QFontMetricsF fm(testFont);
2989+
auto [ascent, descent] = layoutWithAlignment(QTextCharFormat::AlignBaseline);
2990+
QCOMPARE(descent, fm.descent());
2991+
QCOMPARE(ascent, objHeight - fm.descent());
2992+
}
2993+
2994+
// AlignMiddle: centered on x-height/2.
2995+
{
2996+
const QFontMetricsF fm(testFont);
2997+
const qreal halfX = fm.xHeight() / 2.0;
2998+
auto [ascent, descent] = layoutWithAlignment(QTextCharFormat::AlignMiddle);
2999+
QCOMPARE(ascent, (objHeight + halfX) / 2.0);
3000+
QCOMPARE(descent, (objHeight - halfX) / 2.0);
3001+
}
3002+
}
3003+
3004+
#ifdef QT_BUILD_INTERNAL
3005+
void tst_QTextLayout::standaloneInlineObject_cursorToX_data()
3006+
{
3007+
QTest::addColumn<QString>("text");
3008+
QTest::addColumn<QList<int>>("objectPositions");
3009+
QTest::addColumn<int>("cursorPos");
3010+
QTest::addColumn<qreal>("expectedX");
3011+
3012+
const QChar orc = QChar::ObjectReplacementCharacter;
3013+
3014+
// Single object: "A<ORC>B"
3015+
const QString single = QStringLiteral("A") + orc + QStringLiteral("B");
3016+
QTest::newRow("single: before text") << single << QList<int>{1} << 0 << qreal(0);
3017+
QTest::newRow("single: before object") << single << QList<int>{1} << 1 << qreal(TESTFONT_SIZE);
3018+
QTest::newRow("single: after object") << single << QList<int>{1} << 2 << qreal(TESTFONT_SIZE + TEST_OBJECT_WIDTH);
3019+
3020+
// Multiple adjacent objects: "A<ORC><ORC>B"
3021+
const QString multi = QStringLiteral("A") + orc + orc + QStringLiteral("B");
3022+
QTest::newRow("multi: before first object") << multi << QList<int>{1, 2} << 1 << qreal(TESTFONT_SIZE);
3023+
QTest::newRow("multi: between objects") << multi << QList<int>{1, 2} << 2 << qreal(TESTFONT_SIZE + TEST_OBJECT_WIDTH);
3024+
QTest::newRow("multi: after second object") << multi << QList<int>{1, 2} << 3 << qreal(TESTFONT_SIZE + TEST_OBJECT_WIDTH * 2);
3025+
3026+
// Object at start: "<ORC>B"
3027+
const QString atStart = QString(orc) + QStringLiteral("B");
3028+
QTest::newRow("start: before object") << atStart << QList<int>{0} << 0 << qreal(0);
3029+
QTest::newRow("start: after object") << atStart << QList<int>{0} << 1 << qreal(TEST_OBJECT_WIDTH);
3030+
3031+
// Object at end: "A<ORC>"
3032+
const QString atEnd = QStringLiteral("A") + orc;
3033+
QTest::newRow("end: before object") << atEnd << QList<int>{1} << 1 << qreal(TESTFONT_SIZE);
3034+
QTest::newRow("end: after object") << atEnd << QList<int>{1} << 2 << qreal(TESTFONT_SIZE + TEST_OBJECT_WIDTH);
3035+
}
3036+
3037+
void tst_QTextLayout::standaloneInlineObject_cursorToX()
3038+
{
3039+
QFETCH(QString, text);
3040+
QFETCH(QList<int>, objectPositions);
3041+
QFETCH(int, cursorPos);
3042+
QFETCH(qreal, expectedX);
3043+
3044+
QTextLayout layout;
3045+
setupStandaloneLayoutWithObjects(layout, text, testFont, objectPositions,
3046+
TEST_OBJECT_WIDTH, TEST_OBJECT_WIDTH);
3047+
layout.beginLayout();
3048+
QTextLine line = layout.createLine();
3049+
line.setLineWidth(1000);
3050+
layout.endLayout();
3051+
3052+
QCOMPARE(line.cursorToX(cursorPos), expectedX);
3053+
}
3054+
3055+
void tst_QTextLayout::standaloneInlineObject_wrapping()
3056+
{
3057+
// "<ORC>A" — object is 3*TESTFONT_SIZE wide, line width fits the object
3058+
// but not object + 'A', so 'A' must wrap to the second line.
3059+
const QChar orc = QChar::ObjectReplacementCharacter;
3060+
const qreal objWidth = 3 * TESTFONT_SIZE;
3061+
QString text = QString(orc) + QStringLiteral("A");
3062+
3063+
QTextLayout layout;
3064+
setupStandaloneLayoutWithObjects(layout, text, testFont, { 0 }, objWidth, 10);
3065+
3066+
QTextOption opt;
3067+
opt.setWrapMode(QTextOption::WrapAnywhere);
3068+
layout.setTextOption(opt);
3069+
3070+
layout.beginLayout();
3071+
QTextLine line1 = layout.createLine();
3072+
line1.setLineWidth(3.5 * TESTFONT_SIZE);
3073+
QTextLine line2 = layout.createLine();
3074+
if (line2.isValid())
3075+
line2.setLineWidth(3.5 * TESTFONT_SIZE);
3076+
layout.endLayout();
3077+
3078+
QVERIFY(line2.isValid());
3079+
QCOMPARE(line1.textLength(), 1);
3080+
}
3081+
3082+
void tst_QTextLayout::standaloneInlineObject_eliding()
3083+
{
3084+
// "A<ORC>B" — total width exceeds availableWidth, so eliding must truncate.
3085+
const QChar orc = QChar::ObjectReplacementCharacter;
3086+
QString text = QStringLiteral("A") + orc + QStringLiteral("B");
3087+
3088+
QTextLayout layout;
3089+
setupStandaloneLayoutWithObjects(layout, text, testFont, { 1 }, TEST_OBJECT_WIDTH, 16);
3090+
3091+
const qreal availableWidth = TESTFONT_SIZE + TEST_OBJECT_WIDTH;
3092+
3093+
layout.beginLayout();
3094+
QTextLine line = layout.createLine();
3095+
line.setLineWidth(availableWidth);
3096+
layout.endLayout();
3097+
3098+
QString elided = layout.engine()->elidedText(Qt::ElideRight, QFixed::fromReal(availableWidth),
3099+
0, 0, text.size());
3100+
3101+
QVERIFY(!elided.isEmpty());
3102+
QVERIFY(elided.size() < text.size());
3103+
QVERIFY(elided.contains(QChar(0x2026)) || elided.contains(QLatin1String("...")));
3104+
}
3105+
#endif
3106+
28763107
QTEST_MAIN(tst_QTextLayout)
28773108
#include "tst_qtextlayout.moc"

0 commit comments

Comments
 (0)