From 1ee5fed75f07ac63fe6f0463fca72af672304ddb Mon Sep 17 00:00:00 2001 From: Eskil Abrahamsen Blomfeldt Date: Wed, 15 Jan 2020 11:53:35 +0100 Subject: [PATCH] Introduce PathText path element For text rendering in Qt Quick, we currently have the limitation that when rendering text at such a large size that the distance fields start showing artifacts, the only option is to use NativeRendering, which will look nice, but which will use a lot of texture memory for the glyph cache, since it will actually cache the glyphs at the requested size. A suggested approach would be to fall back to using triangulated paths when the font gets large enough, but the work on this was never completed. It turns out that we can get this now, basically for free, since we already support rendering arbitrary QPainterPaths using Qt Quick Shapes. The only thing missing is the ability to add the path of a given text to the shape. This patch fills in that gap. Note that this is currently not supported by nvidia renderer. [ChangeLog][QtQuick] Added PathText path element which can be used together with Qt Quick Shapes to get text rendering that does not cache glyphs in a texture, but triangulates the outlines of the glyphs instead. Change-Id: I436e1476b129b324cf7a54f89a1b18e0579e8185 Reviewed-by: Laszlo Agocs --- examples/quick/shapes/content/item18.qml | 71 ++++++ .../quick/shapes/content/shapegallery.qml | 4 + examples/quick/shapes/shapes.pro | 3 +- examples/quick/shapes/shapes.qrc | 1 + src/quick/util/qquickpath.cpp | 223 ++++++++++++++++++ src/quick/util/qquickpath_p.h | 102 ++++++++ src/quick/util/qquickpath_p_p.h | 1 + 7 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 examples/quick/shapes/content/item18.qml diff --git a/examples/quick/shapes/content/item18.qml b/examples/quick/shapes/content/item18.qml new file mode 100644 index 0000000000..3774d19bc5 --- /dev/null +++ b/examples/quick/shapes/content/item18.qml @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.15 +import QtQuick.Shapes 1.0 + +Rectangle { + color: "lightGray" + + Shape { + anchors.centerIn: parent + width: 200 + height: 100 + + ShapePath { + id: capTest + strokeColor: "black" + strokeWidth: 1 + fillColor: "black" + + PathText { x: 0; y: 100; font.family: "Arial"; font.pixelSize: 150; text: "Qt!" } + } + } +} diff --git a/examples/quick/shapes/content/shapegallery.qml b/examples/quick/shapes/content/shapegallery.qml index 86445e25c2..e5a7c51483 100644 --- a/examples/quick/shapes/content/shapegallery.qml +++ b/examples/quick/shapes/content/shapegallery.qml @@ -130,6 +130,10 @@ Rectangle { name: "Tiger" shapeUrl: "item17.qml" } + ListElement { + name: "Text" + shapeUrl: "item18.qml" + } } property int gridSpacing: 10 diff --git a/examples/quick/shapes/shapes.pro b/examples/quick/shapes/shapes.pro index ff6fa422fb..f99d941804 100644 --- a/examples/quick/shapes/shapes.pro +++ b/examples/quick/shapes/shapes.pro @@ -24,7 +24,8 @@ OTHER_FILES += content/main.qml \ content/item13.qml \ content/item14.qml \ content/item15.qml \ - content/item17.qml + content/item17.qml \ + content/item18.qml target.path = $$[QT_INSTALL_EXAMPLES]/quick/shapes INSTALLS += target diff --git a/examples/quick/shapes/shapes.qrc b/examples/quick/shapes/shapes.qrc index e03c0e8a0a..6de463dd33 100644 --- a/examples/quick/shapes/shapes.qrc +++ b/examples/quick/shapes/shapes.qrc @@ -28,5 +28,6 @@ content/item14.qml content/item15.qml content/item17.qml + content/item18.qml diff --git a/src/quick/util/qquickpath.cpp b/src/quick/util/qquickpath.cpp index c855acc185..375d0265e8 100644 --- a/src/quick/util/qquickpath.cpp +++ b/src/quick/util/qquickpath.cpp @@ -301,6 +301,8 @@ void QQuickPath::pathElements_append(QQmlListProperty *proper QQuickCurve *curve = qobject_cast(pathElement); if (curve) d->_pathCurves.append(curve); + else if (QQuickPathText *text = qobject_cast(pathElement)) + d->_pathTexts.append(text); else { QQuickPathAttribute *attribute = qobject_cast(pathElement); if (attribute && !d->_attributes.contains(attribute->name())) @@ -329,6 +331,7 @@ void QQuickPath::pathElements_clear(QQmlListProperty *propert d->_pathElements.clear(); d->_pathCurves.clear(); d->_pointCache.clear(); + d->_pathTexts.clear(); } void QQuickPath::interpolate(int idx, const QString &name, qreal value) @@ -478,6 +481,8 @@ QPainterPath QQuickPath::createPath(const QPointF &startPoint, const QPointF &en point.values[percentString] = percent->value(); interpolate(attributePoints, attributePoints.count() - 1, percentString, percent->value()); usesPercent = true; + } else if (QQuickPathText *text = qobject_cast(pathElement)) { + text->addToPath(path); } } @@ -547,6 +552,9 @@ QPainterPath QQuickPath::createShapePath(const QPointF &startPoint, const QPoint ++index; } + for (QQuickPathText *text : qAsConst(d->_pathTexts)) + text->addToPath(path); + if (closed) { QPointF end = path.currentPosition(); *closed = startX == end.x() && startY == end.y(); @@ -593,6 +601,8 @@ void QQuickPath::gatherAttributes() for (QQuickPathElement *pathElement : qAsConst(d->_pathElements)) { if (QQuickCurve *curve = qobject_cast(pathElement)) d->_pathCurves.append(curve); + else if (QQuickPathText *text = qobject_cast(pathElement)) + d->_pathTexts.append(text); else if (QQuickPathAttribute *attribute = qobject_cast(pathElement)) attributes.insert(attribute->name()); } @@ -2619,6 +2629,219 @@ void QQuickPathMultiline::addToPath(QPainterPath &path, const QQuickPathData &) } } +/*! + \qmltype PathText + \instantiates QQuickPathText + \inqmlmodule QtQuick + \ingroup qtquick-animation-paths + \brief Defines a string in a specified font. + \since QtQuick 2.15 + + This element defines the shape of a specified string in a specified font. The text's + baseline will be translated to the x and y coordinates, and the outlines from the font + will be added to the path accordingly. + + \qml + PathText { + x: 0 + y: font.pixelSize + font.family: "Arial" + font.pixelSize: 100 + text: "Foobar" + } + \endqml + + \sa Path, QPainterPath::setFillRule, PathPolyline, PathQuad, PathCubic, PathArc, PathAngleArc, PathCurve, PathSvg, PathMove +*/ + +/*! + \qmlproperty real QtQuick::PathText::x + + The horizontal position of the PathText's baseline. +*/ + +/*! + \qmlproperty real QtQuick::PathText::y + + The vertical position of the PathText's baseline. + + \note This property refers to the position of the baseline of the text, not the top of its bounding box. This may + cause some confusion, e.g. when using the PathText with Qt Quick Shapes. See \l FontMetrics for information on how to + get the ascent of a font, which can be used to translate the text into the expected position. +*/ + +/*! + \qmlproperty string QtQuick::PathText::text + + The text for which this PathText should contain the outlines. +*/ + +/*! + \qmlproperty string QtQuick::PathText::font.family + + Sets the family name of the font. + + The family name is case insensitive and may optionally include a foundry name, e.g. "Helvetica [Cronyx]". + If the family is available from more than one foundry and the foundry isn't specified, an arbitrary foundry is chosen. + If the family isn't available a family will be set using the font matching algorithm. +*/ + +/*! + \qmlproperty string QtQuick::PathText::font.styleName + + Sets the style name of the font. + + The style name is case insensitive. If set, the font will be matched against style name instead + of the font properties \l font.weight, \l font.bold and \l font.italic. +*/ + +/*! + \qmlproperty bool QtQuick::PathText::font.bold + + Sets whether the font weight is bold. +*/ + +/*! + \qmlproperty enumeration QtQuick::PathText::font.weight + + Sets the font's weight. + + The weight can be one of: + \list + \li Font.Thin + \li Font.Light + \li Font.ExtraLight + \li Font.Normal - the default + \li Font.Medium + \li Font.DemiBold + \li Font.Bold + \li Font.ExtraBold + \li Font.Black + \endlist + + \qml + PathText { text: "Hello"; font.weight: Font.DemiBold } + \endqml +*/ + +/*! + \qmlproperty bool QtQuick::PathText::font.italic + + Sets whether the font has an italic style. +*/ + +/*! + \qmlproperty bool QtQuick::PathText::font.underline + + Sets whether the text is underlined. +*/ + +/*! + \qmlproperty bool QtQuick::PathText::font.strikeout + + Sets whether the font has a strikeout style. +*/ + +/*! + \qmlproperty real QtQuick::PathText::font.pointSize + + Sets the font size in points. The point size must be greater than zero. +*/ + +/*! + \qmlproperty int QtQuick::PathText::font.pixelSize + + Sets the font size in pixels. + + Using this function makes the font device dependent. + Use \c pointSize to set the size of the font in a device independent manner. +*/ + +/*! + \qmlproperty real QtQuick::PathText::font.letterSpacing + + Sets the letter spacing for the font. + + Letter spacing changes the default spacing between individual letters in the font. + A positive value increases the letter spacing by the corresponding pixels; a negative value decreases the spacing. +*/ + +/*! + \qmlproperty real QtQuick::PathText::font.wordSpacing + + Sets the word spacing for the font. + + Word spacing changes the default spacing between individual words. + A positive value increases the word spacing by a corresponding amount of pixels, + while a negative value decreases the inter-word spacing accordingly. +*/ + +/*! + \qmlproperty enumeration QtQuick::PathText::font.capitalization + + Sets the capitalization for the text. + + \list + \li Font.MixedCase - This is the normal text rendering option where no capitalization change is applied. + \li Font.AllUppercase - This alters the text to be rendered in all uppercase type. + \li Font.AllLowercase - This alters the text to be rendered in all lowercase type. + \li Font.SmallCaps - This alters the text to be rendered in small-caps type. + \li Font.Capitalize - This alters the text to be rendered with the first character of each word as an uppercase character. + \endlist + + \qml + PathText { text: "Hello"; font.capitalization: Font.AllLowercase } + \endqml +*/ + +/*! + \qmlproperty bool QtQuick::PathText::font.kerning + + Enables or disables the kerning OpenType feature when shaping the text. Disabling this may + improve performance when creating or changing the text, at the expense of some cosmetic + features. The default value is true. + + \qml + PathText { text: "OATS FLAVOUR WAY"; font.kerning: false } + \endqml +*/ + +/*! + \qmlproperty bool QtQuick::PathText::font.preferShaping + + Sometimes, a font will apply complex rules to a set of characters in order to + display them correctly. In some writing systems, such as Brahmic scripts, this is + required in order for the text to be legible, but in e.g. Latin script, it is merely + a cosmetic feature. Setting the \c preferShaping property to false will disable all + such features when they are not required, which will improve performance in most cases. + + The default value is true. + + \qml + PathText { text: "Some text"; font.preferShaping: false } + \endqml +*/ + +void QQuickPathText::updatePath() const +{ + if (!_path.isEmpty()) + return; + + _path.addText(_x, _y, _font, _text); + + // Account for distance from baseline to top, since addText() takes baseline position + QRectF brect = _path.boundingRect(); + _path.translate(0.0, -brect.y()); +} + +void QQuickPathText::addToPath(QPainterPath &path) +{ + if (_text.isEmpty()) + return; + updatePath(); + path.addPath(_path); +} + QT_END_NAMESPACE #include "moc_qquickpath_p.cpp" diff --git a/src/quick/util/qquickpath_p.h b/src/quick/util/qquickpath_p.h index ca0495a90d..159c46d13c 100644 --- a/src/quick/util/qquickpath_p.h +++ b/src/quick/util/qquickpath_p.h @@ -63,6 +63,7 @@ QT_REQUIRE_CONFIG(quick_path); #include #include +#include QT_BEGIN_NAMESPACE @@ -598,6 +599,106 @@ public: static QPointF sequentialPointAt(const QPainterPath &path, const qreal &pathLength, const QList &attributePoints, QQuickCachedBezier &prevBez, qreal p, qreal *angle = nullptr); }; +class Q_QUICK_PRIVATE_EXPORT QQuickPathText : public QQuickPathElement +{ + Q_OBJECT + Q_PROPERTY(qreal x READ x WRITE setX NOTIFY xChanged) + Q_PROPERTY(qreal y READ y WRITE setY NOTIFY yChanged) + Q_PROPERTY(qreal width READ width NOTIFY changed) + Q_PROPERTY(qreal height READ height NOTIFY changed) + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) + Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged) + QML_NAMED_ELEMENT(PathText) + QML_ADDED_IN_MINOR_VERSION(15) +public: + QQuickPathText(QObject *parent=nullptr) : QQuickPathElement(parent) + { + connect(this, &QQuickPathText::xChanged, this, &QQuickPathElement::changed); + connect(this, &QQuickPathText::yChanged, this, &QQuickPathElement::changed); + connect(this, &QQuickPathText::textChanged, this, &QQuickPathElement::changed); + connect(this, &QQuickPathText::fontChanged, this, &QQuickPathElement::changed); + + connect(this, &QQuickPathElement::changed, this, &QQuickPathText::invalidate); + } + + void addToPath(QPainterPath &path); + + qreal x() const { return _x; } + qreal y() const { return _y; } + QString text() const { return _text; } + QFont font() const { return _font; } + + void setX(qreal x) + { + if (qFuzzyCompare(_x, x)) + return; + + _x = x; + Q_EMIT xChanged(); + } + + void setY(qreal y) + { + if (qFuzzyCompare(_y, y)) + return; + + _y = y; + Q_EMIT yChanged(); + } + + void setText(const QString &text) + { + if (text == _text) + return; + + _text = text; + Q_EMIT textChanged(); + } + + void setFont(const QFont &font) + { + if (font == _font) + return; + + _font = font; + Q_EMIT fontChanged(); + } + + qreal width() const + { + updatePath(); + return _path.boundingRect().width(); + } + + qreal height() const + { + updatePath(); + return _path.boundingRect().height(); + } + +Q_SIGNALS: + void xChanged(); + void yChanged(); + void textChanged(); + void fontChanged(); + +private Q_SLOTS: + void invalidate() + { + _path.clear(); + } + +private: + void updatePath() const; + + QString _text; + qreal _x = qreal(0.0); + qreal _y = qreal(0.0); + QFont _font; + + mutable QPainterPath _path; +}; + QT_END_NAMESPACE QML_DECLARE_TYPE(QQuickPathElement) @@ -614,5 +715,6 @@ QML_DECLARE_TYPE(QQuickPathSvg) QML_DECLARE_TYPE(QQuickPathPercent) QML_DECLARE_TYPE(QQuickPathPolyline) QML_DECLARE_TYPE(QQuickPath) +QML_DECLARE_TYPE(QQuickPathText) #endif // QQUICKPATH_H diff --git a/src/quick/util/qquickpath_p_p.h b/src/quick/util/qquickpath_p_p.h index e26001ec77..5505b876c1 100644 --- a/src/quick/util/qquickpath_p_p.h +++ b/src/quick/util/qquickpath_p_p.h @@ -80,6 +80,7 @@ public: QList _attributePoints; QStringList _attributes; QList _pathCurves; + QList _pathTexts; mutable QQuickCachedBezier prevBez; QQmlNullableValue startX; QQmlNullableValue startY;