diff --git a/src/quick/items/qquicktext.cpp b/src/quick/items/qquicktext.cpp index 24dd10ac9f598d2815ea39313730da4c6d42ea59..70a1551205853fa113baa26a30890e97659f226d 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 0425c374064be7f906a83d3eb54d405788713d91..cfa37789cc7e8ffd413539378b1f88ab0d8532c7 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 316395b4b233896d06846bf7e636d0504e332fcc..86de5014e09097d7231c8cf4fae7000c53a002c1 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"