summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>2023-03-23 12:45:46 +0100
committerEskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>2023-05-09 23:39:41 +0200
commit6160ea45b689e9d26795a18f155053ac4dc4dd6b (patch)
treea183ff7330bb44227253b8719db90956eed98bcf
parentb1d59d6dd97e99086d15b29765282866500942f3 (diff)
downloadqtbase-6160ea45b689e9d26795a18f155053ac4dc4dd6b.tar.gz
Implement API for enabling / disabling OpenType features
Similar to the font-features-settings in CSS, this is a low-level API that allows you to pass the information to the shaper in order to enable or disable specific font features by name. [ChangeLog][QtGui][Text] Added an API to QFont which makes it possible to enable and disable specific typographic features in OpenType fonts. Change-Id: Ib48c678f3b97a5a562b08ae34dc895800c8885c0 Reviewed-by: Lars Knoll <lars@knoll.priv.no>
-rw-r--r--src/gui/text/qfont.cpp210
-rw-r--r--src/gui/text/qfont.h13
-rw-r--r--src/gui/text/qfont_p.h4
-rw-r--r--src/gui/text/qtextengine.cpp47
-rw-r--r--src/gui/text/qtextengine_p.h11
-rw-r--r--tests/manual/fontfeatures/fontfeatures.pro17
-rw-r--r--tests/manual/fontfeatures/main.cpp14
-rw-r--r--tests/manual/fontfeatures/mainwindow.cpp225
-rw-r--r--tests/manual/fontfeatures/mainwindow.h32
-rw-r--r--tests/manual/fontfeatures/mainwindow.ui116
-rw-r--r--tests/manual/manual.pro1
11 files changed, 673 insertions, 17 deletions
diff --git a/src/gui/text/qfont.cpp b/src/gui/text/qfont.cpp
index 1efe3c06d8..94529badec 100644
--- a/src/gui/text/qfont.cpp
+++ b/src/gui/text/qfont.cpp
@@ -213,7 +213,7 @@ QFontPrivate::QFontPrivate(const QFontPrivate &other)
strikeOut(other.strikeOut), kerning(other.kerning),
capital(other.capital), letterSpacingIsAbsolute(other.letterSpacingIsAbsolute),
letterSpacing(other.letterSpacing), wordSpacing(other.wordSpacing),
- scFont(other.scFont)
+ fontFeatures(other.fontFeatures), scFont(other.scFont)
{
if (scFont && scFont != this)
scFont->ref.ref();
@@ -343,9 +343,20 @@ void QFontPrivate::resolve(uint mask, const QFontPrivate *other)
wordSpacing = other->wordSpacing;
if (! (mask & QFont::CapitalizationResolved))
capital = other->capital;
+
+ if (!(mask & QFont::FontFeaturesResolved))
+ fontFeatures = other->fontFeatures;
}
+void QFontPrivate::setFontFeature(quint32 tag, quint32 value)
+{
+ fontFeatures.insert(tag, value);
+}
+void QFontPrivate::unsetFontFeature(quint32 tag)
+{
+ fontFeatures.remove(tag);
+}
QFontEngineData::QFontEngineData()
@@ -1748,6 +1759,7 @@ bool QFont::operator==(const QFont &f) const
&& f.d->letterSpacingIsAbsolute == d->letterSpacingIsAbsolute
&& f.d->letterSpacing == d->letterSpacing
&& f.d->wordSpacing == d->wordSpacing
+ && f.d->fontFeatures == d->fontFeatures
));
}
@@ -1785,7 +1797,21 @@ bool QFont::operator<(const QFont &f) const
int f1attrs = (f.d->underline << 3) + (f.d->overline << 2) + (f.d->strikeOut<<1) + f.d->kerning;
int f2attrs = (d->underline << 3) + (d->overline << 2) + (d->strikeOut<<1) + d->kerning;
- return f1attrs < f2attrs;
+ if (f1attrs != f2attrs) return f1attrs < f2attrs;
+
+ if (d->fontFeatures.size() != f.d->fontFeatures.size())
+ return f.d->fontFeatures.size() < d->fontFeatures.size();
+
+ auto it = d->fontFeatures.constBegin();
+ auto jt = f.d->fontFeatures.constBegin();
+ for (; it != d->fontFeatures.constEnd(); ++it, ++jt) {
+ if (it.key() != jt.key())
+ return jt.key() < it.key();
+ if (it.value() != jt.value())
+ return jt.value() < it.value();
+ }
+
+ return false;
}
@@ -2206,6 +2232,179 @@ void QFont::cacheStatistics()
{
}
+/*!
+ \since 6.6
+
+ Applies integer values to specific OpenType features when shaping the text based on the contents
+ in \a fontFeatures. This provides advanced access to the font shaping process, and can be used
+ to support font features that are otherwise not covered in the API.
+
+ An OpenType feature is defined by a 32-bit tag (encoded from the four-character name of the
+ table by using the stringToTag() function), as well as an integer value.
+
+ This integer value passed along with the tag in most cases represents a boolean value: A zero
+ value means the feature is disabled, and a non-zero value means it is enabled. For certain
+ font features, however, it may have other intepretations. For example, when applied to the
+ \c salt feature, the value is an index that specifies the stylistic alternative to use.
+
+ For example, the \c frac font feature will convert diagonal fractions separated with a slash
+ (such as \c 1/2) with a different representation. Typically this will involve baking the full
+ fraction into a single character width (such as \c ½).
+
+ If a font supports the \c frac feature, then it can be enabled in the shaper by setting
+ \c{fontFeatures[stringToTag("frac")] = 1} in the font feature map.
+
+ This function will overwrite the current list of explicit font features. Use setFontFeature() or
+ unsetFontFeature() to set or unset individual features.
+
+ \note By default, Qt will enable and disable certain font features based on other font
+ properties. In particular, the \c kern feature will be enabled/disabled depending on the
+ \l kerning() property of the QFont. In addition, all ligature features
+ (\c liga, \c clig, \c dlig, \c hlig) will be disabled if a \l letterSpacing() is applied,
+ but only for writing systems where the use of ligature is cosmetic. For writing systems where
+ ligatures are required, the features will remain in their default state. The values set using
+ setFontFeatures() and related functions will override the default behavior. If, for instance,
+ the \c{fontFeatures[stringToTag("kern")]} is set to 1, then kerning will always be enabled,
+ regardless of whether the kerning property is set to false. Similarly, if it is set to 0, then
+ it will always be disabled. To reset a font feature to its default behavior, you can unset it
+ in the fontFeatures hash, for example by using unsetFontFeature().
+
+ \sa setFontFeature(), unsetFontFeature(), fontFeatures()
+*/
+void QFont::setFontFeatures(const QHash<quint32, quint32> &fontFeatures)
+{
+ d->detachButKeepEngineData(this);
+ d->fontFeatures = fontFeatures;
+ resolve_mask |= QFont::FontFeaturesResolved;
+}
+
+/*!
+ \since 6.6
+ \overload
+
+ Sets the \a value for a specific font feature \a tag. This is an advanced feature which can be
+ used to enable or disable specific OpenType features if they are available in the font. See
+ \l setFontFeatures() for more details.
+
+ \sa setFontFeatures(), unsetFontFeature(), fontFeatures()
+*/
+void QFont::setFontFeature(quint32 tag, quint32 value)
+{
+ d->detachButKeepEngineData(this);
+ d->setFontFeature(tag, value);
+ resolve_mask |= QFont::FontFeaturesResolved;
+}
+
+/*!
+ \since 6.6
+ \overload
+
+ Sets the \a value of a specific \a fontFeature. This is an advanced feature which can be used to
+ enable or disable specific OpenType features if they are available in the font. See
+ \l setFontFeatures() for more details.
+
+ \note This is equivalent to calling setFontFeature(stringToTag(fontFeature), value).
+
+ \sa setFontFeatures(), unsetFontFeature(), fontFeatures()
+*/
+void QFont::setFontFeature(const char *fontFeature, quint32 value)
+{
+ setFontFeature(stringToTag(fontFeature), value);
+}
+
+/*!
+ \since 6.6
+ \overload
+
+ Unsets the \a fontFeature from the map of explicitly enabled/disabled features.
+
+ \note Even if the feature has not previously been added, this will mark the font features map
+ as modified in this QFont, so that it will take precedence when resolving against other fonts.
+
+ Unsetting an existing feature on the QFont reverts behavior to the default. See
+ \l setFontFeatures() for more details.
+
+ \sa setFontFeatures(), setFontFeature(), fontFeatures()
+*/
+void QFont::unsetFontFeature(quint32 tag)
+{
+ d->detachButKeepEngineData(this);
+ d->unsetFontFeature(tag);
+ resolve_mask |= QFont::FontFeaturesResolved;
+}
+
+/*!
+ \since 6.6
+ \overload
+
+ Unsets the \a fontFeature from the map of explicitly enabled/disabled features.
+
+ \note Even if the feature has not previously been added, this will mark the font features map
+ as modified in this QFont, so that it will take precedence when resolving against other fonts.
+
+ Unsetting an existing feature on the QFont reverts behavior to the default. See
+ \l setFontFeatures() for more details.
+
+ \note This is equivalent to calling unsetFontFeature(stringToTag(fontFeature)).
+
+ \sa setFontFeatures(), setFontFeature(), fontFeatures()
+*/
+void QFont::unsetFontFeature(const char *fontFeature)
+{
+ unsetFontFeature(stringToTag(fontFeature));
+}
+
+/*!
+ \since 6.6
+
+ Returns the hash of explicitly set font features in the QFont. By default this map is empty and
+ the shaping process will use default features based on other font or text properties.
+
+ Unsetting an existing feature on the QFont reverts behavior to the default. See
+ \l setFontFeatures() for more details.
+
+ The key of the returned QHash refers to the font table tag as it's encoded in the font
+ file. It can be converted to a QByteArray using the tagToString() function.
+
+ \sa setFontFeatures(), setFontFeature(), unsetFontFeature()
+*/
+QHash<quint32, quint32> QFont::fontFeatures() const
+{
+ return d->fontFeatures;
+}
+
+/*!
+ \since 6.6
+
+ Returns the decoded name for \a tag.
+
+ \sa setFontFeatures(), setFontFeature(), unsetFontFeature(), stringToTag()
+*/
+QByteArray QFont::tagToString(quint32 tag)
+{
+ char str[4] =
+ { char((tag & 0xff000000) >> 24),
+ char((tag & 0x00ff0000) >> 16),
+ char((tag & 0x0000ff00) >> 8),
+ char((tag & 0x000000ff)) };
+ return QByteArray(str, 4);
+}
+
+/*!
+ \since 6.6
+
+ Returns the encoded tag for \a name. The \a name must be a null-terminated string of exactly
+ four characters. Returns 0 on error.
+
+ \sa setFontFeatures(), setFontFeature(), unsetFontFeature(), tagToString()
+*/
+quint32 QFont::stringToTag(const char *name)
+{
+ if (qstrlen(name) != 4)
+ return 0;
+
+ return MAKE_TAG(name[0], name[1], name[2], name[3]);
+}
extern QStringList qt_fallbacksForFamily(const QString &family, QFont::Style style,
QFont::StyleHint styleHint, QChar::Script script);
@@ -2343,6 +2542,8 @@ QDataStream &operator<<(QDataStream &s, const QFont &font)
else
s << font.d->request.families;
}
+ if (s.version() >= QDataStream::Qt_6_6)
+ s << font.d->fontFeatures;
return s;
}
@@ -2457,6 +2658,11 @@ QDataStream &operator>>(QDataStream &s, QFont &font)
else
font.d->request.families = value;
}
+ if (s.version() >= QDataStream::Qt_6_6) {
+ font.d->fontFeatures.clear();
+ s >> font.d->fontFeatures;
+ }
+
return s;
}
diff --git a/src/gui/text/qfont.h b/src/gui/text/qfont.h
index 6a5c890395..6fa5b7b044 100644
--- a/src/gui/text/qfont.h
+++ b/src/gui/text/qfont.h
@@ -126,7 +126,8 @@ public:
HintingPreferenceResolved = 0x8000,
StyleNameResolved = 0x10000,
FamiliesResolved = 0x20000,
- AllPropertiesResolved = 0x3ffff
+ FontFeaturesResolved = 0x40000,
+ AllPropertiesResolved = 0x7ffff
};
Q_ENUM(ResolveProperties)
@@ -206,6 +207,16 @@ public:
void setHintingPreference(HintingPreference hintingPreference);
HintingPreference hintingPreference() const;
+ void setFontFeature(const char *fontFeature, quint32 value);
+ void setFontFeature(quint32 tag, quint32 value);
+ void setFontFeatures(const QHash<quint32, quint32> &fontFeatures);
+ void unsetFontFeature(quint32 tag);
+ void unsetFontFeature(const char *tag);
+ QHash<quint32, quint32> fontFeatures() const;
+
+ static QByteArray tagToString(quint32 tag);
+ static quint32 stringToTag(const char *tagString);
+
// dupicated from QFontInfo
bool exactMatch() const;
diff --git a/src/gui/text/qfont_p.h b/src/gui/text/qfont_p.h
index 13738bb096..ef52410a0c 100644
--- a/src/gui/text/qfont_p.h
+++ b/src/gui/text/qfont_p.h
@@ -164,6 +164,7 @@ public:
QFixed letterSpacing;
QFixed wordSpacing;
+ QHash<quint32, quint32> fontFeatures;
mutable QFontPrivate *scFont;
QFont smallCapsFont() const { return QFont(smallCapsFontPrivate()); }
@@ -178,6 +179,9 @@ public:
static void detachButKeepEngineData(QFont *font);
+ void setFontFeature(quint32 tag, quint32 value);
+ void unsetFontFeature(quint32 tag);
+
private:
QFontPrivate &operator=(const QFontPrivate &) { return *this; }
};
diff --git a/src/gui/text/qtextengine.cpp b/src/gui/text/qtextengine.cpp
index eded3e3f33..a8e70c17bf 100644
--- a/src/gui/text/qtextengine.cpp
+++ b/src/gui/text/qtextengine.cpp
@@ -1404,6 +1404,7 @@ void QTextEngine::shapeText(int item) const
bool kerningEnabled;
bool letterSpacingIsAbsolute;
bool shapingEnabled = false;
+ QHash<quint32, quint32> fontFeatures;
QFixed letterSpacing, wordSpacing;
#ifndef QT_NO_RAWFONT
if (useRawFont) {
@@ -1417,6 +1418,7 @@ void QTextEngine::shapeText(int item) const
wordSpacing = QFixed::fromReal(font.wordSpacing());
letterSpacing = QFixed::fromReal(font.letterSpacing());
letterSpacingIsAbsolute = true;
+ fontFeatures = font.d->fontFeatures;
} else
#endif
{
@@ -1429,6 +1431,7 @@ void QTextEngine::shapeText(int item) const
letterSpacingIsAbsolute = font.d->letterSpacingIsAbsolute;
letterSpacing = font.d->letterSpacing;
wordSpacing = font.d->wordSpacing;
+ fontFeatures = font.d->fontFeatures;
if (letterSpacingIsAbsolute && letterSpacing.value())
letterSpacing *= font.d->dpi / qt_defaultDpiY();
@@ -1482,7 +1485,14 @@ void QTextEngine::shapeText(int item) const
#if QT_CONFIG(harfbuzz)
if (Q_LIKELY(shapingEnabled)) {
- si.num_glyphs = shapeTextWithHarfbuzzNG(si, string, itemLength, fontEngine, itemBoundaries, kerningEnabled, letterSpacing != 0);
+ si.num_glyphs = shapeTextWithHarfbuzzNG(si,
+ string,
+ itemLength,
+ fontEngine,
+ itemBoundaries,
+ kerningEnabled,
+ letterSpacing != 0,
+ fontFeatures);
} else
#endif
{
@@ -1594,7 +1604,8 @@ int QTextEngine::shapeTextWithHarfbuzzNG(const QScriptItem &si,
QFontEngine *fontEngine,
const QList<uint> &itemBoundaries,
bool kerningEnabled,
- bool hasLetterSpacing) const
+ bool hasLetterSpacing,
+ const QHash<quint32, quint32> &fontFeatures) const
{
uint glyphs_shaped = 0;
@@ -1648,14 +1659,24 @@ int QTextEngine::shapeTextWithHarfbuzzNG(const QScriptItem &si,
|| script == QChar::Script_Khmer || script == QChar::Script_Nko);
bool dontLigate = hasLetterSpacing && !scriptRequiresOpenType;
- const hb_feature_t features[5] = {
- { HB_TAG('k','e','r','n'), !!kerningEnabled, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
- { HB_TAG('l','i','g','a'), false, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
- { HB_TAG('c','l','i','g'), false, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
- { HB_TAG('d','l','i','g'), false, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
- { HB_TAG('h','l','i','g'), false, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }
- };
- const int num_features = dontLigate ? 5 : 1;
+
+ QHash<quint32, quint32> features;
+ features.insert(HB_TAG('k','e','r','n'), !!kerningEnabled);
+ if (dontLigate) {
+ features.insert(HB_TAG('l','i','g','a'), false);
+ features.insert(HB_TAG('c','l','i','g'), false);
+ features.insert(HB_TAG('d','l','i','g'), false);
+ features.insert(HB_TAG('h','l','i','g'), false);
+ }
+ features.insert(fontFeatures);
+
+ QVarLengthArray<hb_feature_t, 16> featureArray;
+ for (auto it = features.constBegin(); it != features.constEnd(); ++it) {
+ featureArray.append({ it.key(),
+ it.value(),
+ HB_FEATURE_GLOBAL_START,
+ HB_FEATURE_GLOBAL_END });
+ }
// whitelist cross-platforms shapers only
static const char *shaper_list[] = {
@@ -1665,7 +1686,11 @@ int QTextEngine::shapeTextWithHarfbuzzNG(const QScriptItem &si,
nullptr
};
- bool shapedOk = hb_shape_full(hb_font, buffer, features, num_features, shaper_list);
+ bool shapedOk = hb_shape_full(hb_font,
+ buffer,
+ featureArray.constData(),
+ features.size(),
+ shaper_list);
if (Q_UNLIKELY(!shapedOk)) {
hb_buffer_destroy(buffer);
return 0;
diff --git a/src/gui/text/qtextengine_p.h b/src/gui/text/qtextengine_p.h
index 59e332c64a..a6015cc311 100644
--- a/src/gui/text/qtextengine_p.h
+++ b/src/gui/text/qtextengine_p.h
@@ -621,9 +621,14 @@ private:
void addRequiredBoundaries() const;
void shapeText(int item) const;
#if QT_CONFIG(harfbuzz)
- int shapeTextWithHarfbuzzNG(const QScriptItem &si, const ushort *string, int itemLength,
- QFontEngine *fontEngine, const QList<uint> &itemBoundaries,
- bool kerningEnabled, bool hasLetterSpacing) const;
+ int shapeTextWithHarfbuzzNG(const QScriptItem &si,
+ const ushort *string,
+ int itemLength,
+ QFontEngine *fontEngine,
+ const QList<uint> &itemBoundaries,
+ bool kerningEnabled,
+ bool hasLetterSpacing,
+ const QHash<quint32, quint32> &fontFeatures) const;
#endif
int endOfLine(int lineNum);
diff --git a/tests/manual/fontfeatures/fontfeatures.pro b/tests/manual/fontfeatures/fontfeatures.pro
new file mode 100644
index 0000000000..62769072b4
--- /dev/null
+++ b/tests/manual/fontfeatures/fontfeatures.pro
@@ -0,0 +1,17 @@
+TEMPLATE = app
+TARGET = fontfeatures
+INCLUDEPATH += .
+QT += widgets
+
+# You can make your code fail to compile if you use deprecated APIs.
+# In order to do so, uncomment the following line.
+# Please consult the documentation of the deprecated API in order to know
+# how to port your code away from it.
+# You can also select to disable deprecated APIs only up to a certain version of Qt.
+#DEFINES += QT_DISABLE_DEPRECATED_UP_TO=0x060000 # disables all APIs deprecated in Qt 6.0.0 and earlier
+
+# Input
+HEADERS += mainwindow.h
+FORMS += mainwindow.ui
+SOURCES += main.cpp \
+ mainwindow.cpp \
diff --git a/tests/manual/fontfeatures/main.cpp b/tests/manual/fontfeatures/main.cpp
new file mode 100644
index 0000000000..9c5bc17874
--- /dev/null
+++ b/tests/manual/fontfeatures/main.cpp
@@ -0,0 +1,14 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "mainwindow.h"
+
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+ QApplication a(argc, argv);
+ MainWindow w;
+ w.show();
+ return a.exec();
+}
diff --git a/tests/manual/fontfeatures/mainwindow.cpp b/tests/manual/fontfeatures/mainwindow.cpp
new file mode 100644
index 0000000000..5342f7c89b
--- /dev/null
+++ b/tests/manual/fontfeatures/mainwindow.cpp
@@ -0,0 +1,225 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+ : QMainWindow(parent)
+ , ui(new Ui::MainWindow)
+{
+ ui->setupUi(this);
+
+ setup();
+ updateSampleText();
+}
+
+MainWindow::~MainWindow()
+{
+ delete ui;
+}
+
+void MainWindow::updateSampleText()
+{
+ QFont font = ui->fontComboBox->currentFont();
+ font.setPixelSize(54);
+
+ for (int i = 0; i < ui->lwFeatures->count(); ++i) {
+ QListWidgetItem *it = ui->lwFeatures->item(i);
+ if (it->checkState() != Qt::PartiallyChecked) {
+ QByteArray ba = it->text().toLatin1();
+ font.setFontFeature(ba, !!it->checkState());
+ }
+ }
+
+ ui->lSampleDisplay->setFont(font);
+ ui->lSampleDisplay->setText(ui->leSampleText->text());
+}
+
+void MainWindow::enableAll()
+{
+ for (int i = 0; i < ui->lwFeatures->count(); ++i) {
+ QListWidgetItem *it = ui->lwFeatures->item(i);
+ it->setCheckState(Qt::Checked);
+ }
+}
+
+void MainWindow::disableAll()
+{
+ for (int i = 0; i < ui->lwFeatures->count(); ++i) {
+ QListWidgetItem *it = ui->lwFeatures->item(i);
+ it->setCheckState(Qt::Unchecked);
+ }
+}
+
+void MainWindow::reset()
+{
+ for (int i = 0; i < ui->lwFeatures->count(); ++i) {
+ QListWidgetItem *it = ui->lwFeatures->item(i);
+ it->setCheckState(Qt::PartiallyChecked);
+ }
+}
+
+void MainWindow::setup()
+{
+ connect(ui->fontComboBox, &QFontComboBox::currentFontChanged, this, &MainWindow::updateSampleText);
+ connect(ui->leSampleText, &QLineEdit::textChanged, this, &MainWindow::updateSampleText);
+ connect(ui->lwFeatures, &QListWidget::itemChanged, this, &MainWindow::updateSampleText);
+ connect(ui->pbEnableAll, &QPushButton::clicked, this, &MainWindow::enableAll);
+ connect(ui->pbDisableAll, &QPushButton::clicked, this, &MainWindow::disableAll);
+ connect(ui->pbReset, &QPushButton::clicked, this, &MainWindow::reset);
+
+ QList<QByteArray> featureList =
+ {
+ "aalt",
+ "abvf",
+ "abvm",
+ "abvs",
+ "afrc",
+ "akhn",
+ "blwf",
+ "blwm",
+ "blws",
+ "calt",
+ "case",
+ "ccmp",
+ "cfar",
+ "chws",
+ "cjct",
+ "clig",
+ "cpct",
+ "cpsp",
+ "cswh",
+ "curs",
+ "cv01",
+ "c2pc",
+ "c2sc",
+ "dist",
+ "dlig",
+ "dnom",
+ "dtls",
+ "expt",
+ "falt",
+ "fin2",
+ "fin3",
+ "fina",
+ "flac",
+ "frac",
+ "fwid",
+ "half",
+ "haln",
+ "halt",
+ "hist",
+ "hkna",
+ "hlig",
+ "hngl",
+ "hojo",
+ "hwid",
+ "init",
+ "isol",
+ "ital",
+ "jalt",
+ "jp78",
+ "jp83",
+ "jp90",
+ "jp04",
+ "kern",
+ "lfbd",
+ "liga",
+ "ljmo",
+ "lnum",
+ "locl",
+ "ltra",
+ "ltrm",
+ "mark",
+ "med2",
+ "medi",
+ "mgrk",
+ "mkmk",
+ "mset",
+ "nalt",
+ "nlck",
+ "nukt",
+ "numr",
+ "onum",
+ "opbd",
+ "ordn",
+ "ornm",
+ "palt",
+ "pcap",
+ "pkna",
+ "pnum",
+ "pref",
+ "pres",
+ "pstf",
+ "psts",
+ "pwid",
+ "qwid",
+ "rand",
+ "rclt",
+ "rkrf",
+ "rlig",
+ "rphf",
+ "rtbd",
+ "rtla",
+ "rtlm",
+ "ruby",
+ "rvrn",
+ "salt",
+ "sinf",
+ "size",
+ "smcp",
+ "smpl",
+ "ss01",
+ "ss02",
+ "ss03",
+ "ss04",
+ "ss05",
+ "ss06",
+ "ss07",
+ "ss08",
+ "ss09",
+ "ss10",
+ "ss11",
+ "ss12",
+ "ss13",
+ "ss14",
+ "ss15",
+ "ss16",
+ "ss17",
+ "ss18",
+ "ss19",
+ "ss20",
+ "ssty",
+ "stch",
+ "subs",
+ "sups",
+ "swsh",
+ "titl",
+ "tjmo",
+ "tnam",
+ "tnum",
+ "trad",
+ "twid",
+ "unic",
+ "valt",
+ "vatu",
+ "vchw",
+ "vert",
+ "vhal",
+ "vjmo",
+ "vkna",
+ "vkrn",
+ "vpal",
+ "vrt2",
+ "vrtr",
+ "zero"
+ };
+
+ for (auto it = featureList.constBegin(); it != featureList.constEnd(); ++it) {
+ QListWidgetItem *item = new QListWidgetItem(*it);
+ item->setFlags(Qt::ItemIsUserTristate | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
+ item->setCheckState(Qt::PartiallyChecked);
+ ui->lwFeatures->addItem(item);
+ }
+}
diff --git a/tests/manual/fontfeatures/mainwindow.h b/tests/manual/fontfeatures/mainwindow.h
new file mode 100644
index 0000000000..c8248e7558
--- /dev/null
+++ b/tests/manual/fontfeatures/mainwindow.h
@@ -0,0 +1,32 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class MainWindow; }
+QT_END_NAMESPACE
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ MainWindow(QWidget *parent = nullptr);
+ ~MainWindow();
+
+private slots:
+ void updateSampleText();
+ void enableAll();
+ void disableAll();
+ void reset();
+
+private:
+ Ui::MainWindow *ui;
+
+ void setup();
+};
+#endif // MAINWINDOW_H
diff --git a/tests/manual/fontfeatures/mainwindow.ui b/tests/manual/fontfeatures/mainwindow.ui
new file mode 100644
index 0000000000..17f56c5a01
--- /dev/null
+++ b/tests/manual/fontfeatures/mainwindow.ui
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>MainWindow</string>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QFontComboBox" name="fontComboBox"/>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Sample text:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="leSampleText">
+ <property name="text">
+ <string>Foobar</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QPushButton" name="pbEnableAll">
+ <property name="text">
+ <string>Enable all</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pbDisableAll">
+ <property name="text">
+ <string>Disable all</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pbReset">
+ <property name="text">
+ <string>Reset</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QListWidget" name="lwFeatures">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="flow">
+ <enum>QListView::TopToBottom</enum>
+ </property>
+ <property name="isWrapping" stdset="0">
+ <bool>false</bool>
+ </property>
+ <property name="viewMode">
+ <enum>QListView::ListMode</enum>
+ </property>
+ <property name="uniformItemSizes">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLabel" name="lSampleDisplay">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>LABEL</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QMenuBar" name="menubar">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>21</height>
+ </rect>
+ </property>
+ </widget>
+ <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/tests/manual/manual.pro b/tests/manual/manual.pro
index 7bb92bb453..3519fc1148 100644
--- a/tests/manual/manual.pro
+++ b/tests/manual/manual.pro
@@ -5,6 +5,7 @@ SUBDIRS = \
filetest \
embeddedintoforeignwindow \
foreignwindows \
+fontfeatures \
gestures \
highdpi \
inputmethodhints \