From b20df2bf54aec02f46d960868562cf1b100d455c Mon Sep 17 00:00:00 2001 From: Andrew den Exter <andrew.den-exter@nokia.com> Date: Mon, 26 Mar 2012 15:54:16 +1000 Subject: [PATCH] Fix Text baselineOffset caclulations. Update the baselineOffset when short cutting layout due to an empty text property. And allow alterations to the baseline due to images, font scaling and custom layouts when doing a layout. Task-number: QTBUG-24303 Change-Id: I5a31a6108cded490fef8b0674e15558ea4e22d6b Reviewed-by: Michael Brasser <michael.brasser@nokia.com> --- src/quick/items/qquicktext.cpp | 62 +++-- src/quick/items/qquicktext_p_p.h | 3 +- .../auto/quick/qquicktext/tst_qquicktext.cpp | 250 +++++++++++++++++- 3 files changed, 295 insertions(+), 20 deletions(-) diff --git a/src/quick/items/qquicktext.cpp b/src/quick/items/qquicktext.cpp index 24dd10ac9f..70a1551205 100644 --- a/src/quick/items/qquicktext.cpp +++ b/src/quick/items/qquicktext.cpp @@ -382,6 +382,22 @@ void QQuickText::imageDownloadFinished() } } +void QQuickTextPrivate::updateBaseline(qreal baseline, qreal dy) +{ + Q_Q(QQuickText); + + qreal yoff = 0; + + if (q->heightValid()) { + if (vAlign == QQuickText::AlignBottom) + yoff = dy; + else if (vAlign == QQuickText::AlignVCenter) + yoff = dy/2; + } + + q->setBaselineOffset(baseline + yoff); +} + void QQuickTextPrivate::updateSize() { Q_Q(QQuickText); @@ -398,9 +414,19 @@ void QQuickTextPrivate::updateSize() return; } - QFontMetricsF fm(font); - if (text.isEmpty()) { - qreal fontHeight = fm.height(); + if (text.isEmpty() && !isLineLaidOutConnected() && fontSizeMode() == QQuickText::FixedSize) { + // How much more expensive is it to just do a full layout on an empty string here? + // There may be subtle differences in the height and baseline calculations between + // QTextLayout and QFontMetrics and the number of variables that can affect the size + // and position of a line is increasing. + QFontMetricsF fm(font); + qreal fontHeight = qCeil(fm.height()); // QScriptLine and therefore QTextLine rounds up + if (!richText) { // line height, so we will as well. + fontHeight = lineHeightMode() == QQuickText::FixedHeight + ? lineHeight() + : fontHeight * lineHeight(); + } + updateBaseline(fm.ascent(), q->height() - fontHeight); q->setImplicitSize(0, fontHeight); layedOutTextRect = QRectF(0, 0, 0, fontHeight); emit q->contentSizeChanged(); @@ -411,7 +437,6 @@ void QQuickTextPrivate::updateSize() qreal naturalWidth = 0; - qreal dy = q->height(); QSizeF size(0, 0); QSizeF previousSize = layedOutTextRect.size(); #if defined(Q_OS_MAC) @@ -420,14 +445,15 @@ void QQuickTextPrivate::updateSize() //setup instance of QTextLayout for all cases other than richtext if (!richText) { - QRectF textRect = setupTextLayout(&naturalWidth); + qreal baseline = 0; + QRectF textRect = setupTextLayout(&naturalWidth, &baseline); if (internalWidthUpdate) // probably the result of a binding loop, but by letting it return; // get this far we'll get a warning to that effect if it is. layedOutTextRect = textRect; size = textRect.size(); - dy -= size.height(); + updateBaseline(baseline, q->height() - size.height()); } else { singleline = false; // richtext can't elide or be optimized for single-line case ensureDoc(); @@ -458,20 +484,13 @@ void QQuickTextPrivate::updateSize() extra->doc->setTextWidth(q->width()); else extra->doc->setTextWidth(extra->doc->idealWidth()); // ### Text does not align if width is not set (QTextDoc bug) - dy -= extra->doc->size().height(); QSizeF dsize = extra->doc->size(); layedOutTextRect = QRectF(QPointF(0,0), dsize); size = QSizeF(extra->doc->idealWidth(),dsize.height()); - } - qreal yoff = 0; - if (q->heightValid()) { - if (vAlign == QQuickText::AlignBottom) - yoff = dy; - else if (vAlign == QQuickText::AlignVCenter) - yoff = dy/2; + QFontMetricsF fm(font); + updateBaseline(fm.ascent(), q->height() - size.height()); } - q->setBaselineOffset(fm.ascent() + yoff); //### need to comfirm cost of always setting these for richText internalWidthUpdate = true; @@ -669,7 +688,7 @@ QString QQuickTextPrivate::elidedText(qreal lineWidth, const QTextLine &line, QT already absolutely positioned horizontally). */ -QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth) +QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth, qreal *const baseline) { Q_Q(QQuickText); layout.setCacheEnabled(true); @@ -712,9 +731,10 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth) internalWidthUpdate = wasInLayout; } - QFontMetrics fm(font); + QFontMetricsF fm(font); qreal height = (lineHeightMode() == QQuickText::FixedHeight) ? lineHeight() : fm.height() * lineHeight(); - return QRect(0, 0, 0, height); + *baseline = fm.ascent(); + return QRectF(0, 0, 0, height); } qreal lineWidth = q->widthValid() && q->width() > 0 ? q->width() : FLT_MAX; @@ -1024,6 +1044,12 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth) elideLayout = 0; } + QTextLine firstLine = visibleCount == 1 && elideLayout + ? elideLayout->lineAt(0) + : layout.lineAt(0); + Q_ASSERT(firstLine.isValid()); + *baseline = firstLine.y() + firstLine.ascent(); + if (!customLayout) br.setHeight(height); diff --git a/src/quick/items/qquicktext_p_p.h b/src/quick/items/qquicktext_p_p.h index 0425c37406..cfa37789cc 100644 --- a/src/quick/items/qquicktext_p_p.h +++ b/src/quick/items/qquicktext_p_p.h @@ -75,6 +75,7 @@ public: ~QQuickTextPrivate(); void init(); + void updateBaseline(qreal baseline, qreal dy); void updateSize(); void updateLayout(); bool determineHorizontalAlignment(); @@ -163,7 +164,7 @@ public: void ensureDoc(); - QRectF setupTextLayout(qreal *const naturalWidth); + QRectF setupTextLayout(qreal *const naturalWidth, qreal * const baseline); void setupCustomLineGeometry(QTextLine &line, qreal &height, int lineOffset = 0); bool isLinkActivatedConnected(); QString anchorAt(const QPointF &pos); diff --git a/tests/auto/quick/qquicktext/tst_qquicktext.cpp b/tests/auto/quick/qquicktext/tst_qquicktext.cpp index 316395b4b2..86de5014e0 100644 --- a/tests/auto/quick/qquicktext/tst_qquicktext.cpp +++ b/tests/auto/quick/qquicktext/tst_qquicktext.cpp @@ -131,6 +131,9 @@ private slots: void fontFormatSizes_data(); void fontFormatSizes(); + void baselineOffset_data(); + void baselineOffset(); + private: QStringList standard; QStringList richText; @@ -1692,7 +1695,7 @@ void tst_qquicktext::boundingRect() QCOMPARE(text->boundingRect().x(), qreal(0)); QCOMPARE(text->boundingRect().y(), qreal(0)); QCOMPARE(text->boundingRect().width(), qreal(0)); - QCOMPARE(text->boundingRect().height(), QFontMetricsF(text->font()).height()); + QCOMPARE(text->boundingRect().height(), qreal(qCeil(QFontMetricsF(text->font()).height()))); text->setText("Hello World"); @@ -2592,6 +2595,251 @@ void tst_qquicktext::fontFormatSizes() delete view; } +typedef qreal (*ExpectedBaseline)(QQuickText *item); +Q_DECLARE_METATYPE(ExpectedBaseline) + +static qreal expectedBaselineTop(QQuickText *item) +{ + QFontMetricsF fm(item->font()); + return fm.ascent(); +} + +static qreal expectedBaselineBottom(QQuickText *item) +{ + QFontMetricsF fm(item->font()); + return item->height() - item->contentHeight() + fm.ascent(); +} + +static qreal expectedBaselineCenter(QQuickText *item) +{ + QFontMetricsF fm(item->font()); + return ((item->height() - item->contentHeight()) / 2) + fm.ascent(); +} + +static qreal expectedBaselineBold(QQuickText *item) +{ + QFont font = item->font(); + font.setBold(true); + QFontMetricsF fm(font); + return fm.ascent(); +} + +static qreal expectedBaselineImage(QQuickText *item) +{ + QFontMetricsF fm(item->font()); + // The line is positioned so the bottom of the line is aligned with the bottom of the image, + // or image height - line height and the baseline is line position + ascent. Because + // QTextLine's height is rounded up this can give slightly different results to image height + // - descent. + return 181 - qCeil(fm.height()) + fm.ascent(); +} + +static qreal expectedBaselineCustom(QQuickText *item) +{ + QFontMetricsF fm(item->font()); + return 16 + fm.ascent(); +} + +static qreal expectedBaselineScaled(QQuickText *item) +{ + QFont font = item->font(); + QTextLayout layout(item->text().replace(QLatin1Char('\n'), QChar::LineSeparator)); + do { + layout.setFont(font); + qreal width = 0; + layout.beginLayout(); + for (QTextLine line = layout.createLine(); line.isValid(); line = layout.createLine()) { + line.setLineWidth(FLT_MAX); + width = qMax(line.naturalTextWidth(), width); + } + layout.endLayout(); + + if (width < item->width()) { + QFontMetricsF fm(layout.font()); + return fm.ascent(); + } + font.setPointSize(font.pointSize() - 1); + } while (font.pointSize() > 0); + return 0; +} + +static qreal expectedBaselineFixedBottom(QQuickText *item) +{ + QFontMetricsF fm(item->font()); + qreal dy = item->text().contains(QLatin1Char('\n')) + ? 160 + : 180; + return dy + fm.ascent(); +} + +static qreal expectedBaselineProportionalBottom(QQuickText *item) +{ + QFontMetricsF fm(item->font()); + qreal dy = item->text().contains(QLatin1Char('\n')) + ? 200 - (qCeil(fm.height()) * 3) + : 200 - (qCeil(fm.height()) * 1.5); + return dy + fm.ascent(); +} + +void tst_qquicktext::baselineOffset_data() +{ + qRegisterMetaType<ExpectedBaseline>(); + QTest::addColumn<QString>("text"); + QTest::addColumn<QString>("wrappedText"); + QTest::addColumn<QByteArray>("bindings"); + QTest::addColumn<ExpectedBaseline>("expectedBaseline"); + QTest::addColumn<ExpectedBaseline>("expectedBaselineEmpty"); + + QTest::newRow("top align") + << "hello world" + << "hello\nworld" + << QByteArray("height: 200; verticalAlignment: Text.AlignTop") + << &expectedBaselineTop + << &expectedBaselineTop; + QTest::newRow("bottom align") + << "hello world" + << "hello\nworld" + << QByteArray("height: 200; verticalAlignment: Text.AlignBottom") + << &expectedBaselineBottom + << &expectedBaselineBottom; + QTest::newRow("center align") + << "hello world" + << "hello\nworld" + << QByteArray("height: 200; verticalAlignment: Text.AlignVCenter") + << &expectedBaselineCenter + << &expectedBaselineCenter; + + QTest::newRow("bold") + << "<b>hello world</b>" + << "<b>hello<br/>world</b>" + << QByteArray("height: 200") + << &expectedBaselineTop + << &expectedBaselineBold; + + QTest::newRow("richText") + << "<b>hello world</b>" + << "<b>hello<br/>world</b>" + << QByteArray("height: 200; textFormat: Text.RichText") + << &expectedBaselineTop + << &expectedBaselineTop; + + QTest::newRow("elided") + << "hello world" + << "hello\nworld" + << QByteArray("width: 20; height: 8; elide: Text.ElideRight") + << &expectedBaselineTop + << &expectedBaselineTop; + + QTest::newRow("elided bottom align") + << "hello world" + << "hello\nworld!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + << QByteArray("width: 200; height: 200; elide: Text.ElideRight; verticalAlignment: Text.AlignBottom") + << &expectedBaselineBottom + << &expectedBaselineBottom; + + QTest::newRow("image") + << "hello <img src=\"images/heart200.png\" /> world" + << "hello <img src=\"images/heart200.png\" /><br/>world" + << QByteArray("height: 200\n; baseUrl: \"") + testFileUrl("reference").toEncoded() + QByteArray("\"") + << &expectedBaselineImage + << &expectedBaselineTop; + + QTest::newRow("customLine") + << "hello world" + << "hello\nworld" + << QByteArray("height: 200; onLineLaidOut: line.y += 16") + << &expectedBaselineCustom + << &expectedBaselineCustom; + + QTest::newRow("scaled font") + << "hello world" + << "hello\nworld" + << QByteArray("width: 200; minimumPointSize: 1; font.pointSize: 64; fontSizeMode: Text.HorizontalFit") + << &expectedBaselineScaled + << &expectedBaselineTop; + + QTest::newRow("fixed line height top align") + << "hello world" + << "hello\nworld" + << QByteArray("height: 200; lineHeightMode: Text.FixedHeight; lineHeight: 20; verticalAlignment: Text.AlignTop") + << &expectedBaselineTop + << &expectedBaselineTop; + + QTest::newRow("fixed line height bottom align") + << "hello world" + << "hello\nworld" + << QByteArray("height: 200; lineHeightMode: Text.FixedHeight; lineHeight: 20; verticalAlignment: Text.AlignBottom") + << &expectedBaselineFixedBottom + << &expectedBaselineFixedBottom; + + QTest::newRow("proportional line height top align") + << "hello world" + << "hello\nworld" + << QByteArray("height: 200; lineHeightMode: Text.ProportionalHeight; lineHeight: 1.5; verticalAlignment: Text.AlignTop") + << &expectedBaselineTop + << &expectedBaselineTop; + + QTest::newRow("proportional line height bottom align") + << "hello world" + << "hello\nworld" + << QByteArray("height: 200; lineHeightMode: Text.ProportionalHeight; lineHeight: 1.5; verticalAlignment: Text.AlignBottom") + << &expectedBaselineProportionalBottom + << &expectedBaselineProportionalBottom; +} + +void tst_qquicktext::baselineOffset() +{ + QFETCH(QString, text); + QFETCH(QString, wrappedText); + QFETCH(QByteArray, bindings); + QFETCH(ExpectedBaseline, expectedBaseline); + QFETCH(ExpectedBaseline, expectedBaselineEmpty); + + QQmlComponent component(&engine); + component.setData( + "import QtQuick 2.0\n" + "Text {\n" + + bindings + "\n" + "}", QUrl()); + + QScopedPointer<QObject> object(component.create()); + + QQuickText *item = qobject_cast<QQuickText *>(object.data()); + QVERIFY(item); + + { + qreal baseline = expectedBaselineEmpty(item); + + QCOMPARE(item->baselineOffset(), baseline); + + item->setText(text); + if (expectedBaseline != expectedBaselineEmpty) + baseline = expectedBaseline(item); + + QCOMPARE(item->baselineOffset(), baseline); + + item->setText(wrappedText); + QCOMPARE(item->baselineOffset(), expectedBaseline(item)); + } + + QFont font = item->font(); + font.setPointSize(font.pointSize() + 8); + + { + QCOMPARE(item->baselineOffset(), expectedBaseline(item)); + + item->setText(text); + qreal baseline = expectedBaseline(item); + QCOMPARE(item->baselineOffset(), baseline); + + item->setText(QString()); + if (expectedBaselineEmpty != expectedBaseline) + baseline = expectedBaselineEmpty(item); + + QCOMPARE(item->baselineOffset(), baseline); + } +} + QTEST_MAIN(tst_qquicktext) #include "tst_qquicktext.moc" -- GitLab