Fix Text baselineOffset caclulations.
authorAndrew den Exter <andrew.den-exter@nokia.com>
Mon, 26 Mar 2012 05:54:16 +0000 (15:54 +1000)
committerQt by Nokia <qt-info@nokia.com>
Mon, 16 Apr 2012 23:24:38 +0000 (01:24 +0200)
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
src/quick/items/qquicktext_p_p.h
tests/auto/quick/qquicktext/tst_qquicktext.cpp

index 24dd10a..70a1551 100644 (file)
@@ -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);
 
index 0425c37..cfa3778 100644 (file)
@@ -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);
index 316395b..86de501 100644 (file)
@@ -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"