File indexing completed on 2024-12-01 06:44:13
0001 // SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0002 // SPDX-FileCopyrightText: 2022-2023 Harald Sitter <sitter@kde.org> 0003 0004 #include "kcountryflagemojiiconengine.h" 0005 0006 #include <QDebug> 0007 #include <QFont> 0008 #include <QGuiApplication> 0009 #include <QPainter> 0010 #include <QPalette> 0011 0012 using namespace Qt::Literals::StringLiterals; 0013 0014 namespace 0015 { 0016 0017 Q_GLOBAL_STATIC(QFont, s_globalDefaultFont, "emoji"_L1) 0018 0019 QString makeCountryEmoji(const QString &country) 0020 { 0021 // The way this was set up by unicode is actually pretty smart. Country flags are based on their two character 0022 // country codes within a given range of code points. And even better, the offset inside the range is the same 0023 // as the offset inside ASCII. Meaning the offset of 'A' from 0 is the same as the offset of π¦ in the 0024 // flag codepoint range. The way a flag is then denoted is e.g. <SURROGATEPAIR>π¦<SURROGATEPAIR>πΉ resulting in 0025 // the Austrian flag. 0026 // https://en.wikipedia.org/wiki/Regional_indicator_symbol 0027 0028 static constexpr auto surrogatePairCodePoint = 0xD83C; // U+D83C 0029 static constexpr auto flagCodePointStart = 0xDDE6; // U+1F1E6 (π¦) - NB: we are in UTF-16 0030 static constexpr auto offsetCodePointA = 'A'_L1.unicode(); // offset from 0, the flag code points have the same offsets 0031 static constexpr auto basePoint = flagCodePointStart - offsetCodePointA; 0032 0033 QString emoji; 0034 emoji.reserve(2 * country.size()); 0035 for (const auto &c : country) { 0036 emoji.append(QChar(surrogatePairCodePoint)); 0037 emoji.append(QChar(basePoint + c.toUpper().unicode())); 0038 } 0039 0040 return emoji; 0041 } 0042 0043 QString makeRegionEmoji(const QString ®ion) 0044 { 0045 // Region flags work much the same as country flags but with a slightly different format in a slightly different 0046 // code point region. Specifically they use ISO 3166-2 as input (e.g. GB-SCT for Scotland). It all happens in 0047 // the Unicode Block βTagsβ (starting at U+E0000) wherein it functions the same as the country codes do in their 0048 // block. The offsets inside the block are the same as the ascii offsets and the emoji is constructed by combining 0049 // the off set code points of the incoming region tag. They are prefixed with U+1F3F4 π΄ WAVING BLACK FLAG 0050 // and suffixed with U+E007F CANCEL TAG. 0051 // https://en.wikipedia.org/wiki/Regional_indicator_symbol 0052 0053 auto hyphenlessRegion = region; 0054 hyphenlessRegion.remove('-'_L1); 0055 0056 static constexpr auto surrogatePairCodePoint = 0xdb40; // U+DB40 0057 static constexpr auto flagCodePointStart = 0xDC41; // U+E0041 (Tag Latin Capital Letter A) - NB: we are in UTF-16 0058 static constexpr auto offsetCodePointA = 'A'_L1.unicode(); // offset from 0, the flag code points have the same offsets 0059 static constexpr auto basePoint = flagCodePointStart - offsetCodePointA; 0060 0061 auto emoji = u"π΄"_s; 0062 emoji.reserve(emoji.size() + 2 * hyphenlessRegion.size() + 2); 0063 for (const auto &c : hyphenlessRegion) { 0064 emoji.append(QChar(surrogatePairCodePoint)); 0065 emoji.append(QChar(basePoint + c.toLower().unicode())); 0066 } 0067 static const auto cancelTag = QString().append(QChar(surrogatePairCodePoint)).append(QChar(0xDC7F)); 0068 return emoji.append(cancelTag); 0069 } 0070 0071 } // namespace 0072 0073 class Q_DECL_HIDDEN KCountryFlagEmojiIconEnginePrivate 0074 { 0075 public: 0076 explicit KCountryFlagEmojiIconEnginePrivate(const QString ®ionOrCountry) 0077 : m_country(regionOrCountry) 0078 , m_emoji(regionOrCountry.contains("-"_L1) ? makeRegionEmoji(regionOrCountry) : makeCountryEmoji(regionOrCountry)) 0079 { 0080 } 0081 0082 const QString m_country; 0083 const QString m_emoji; 0084 }; 0085 0086 KCountryFlagEmojiIconEngine::KCountryFlagEmojiIconEngine(const QString &country) 0087 : d(std::make_unique<KCountryFlagEmojiIconEnginePrivate>(country)) 0088 { 0089 } 0090 0091 KCountryFlagEmojiIconEngine::~KCountryFlagEmojiIconEngine() = default; 0092 0093 QIconEngine *KCountryFlagEmojiIconEngine::clone() const 0094 { 0095 return new KCountryFlagEmojiIconEngine(d->m_country); 0096 } 0097 0098 QString KCountryFlagEmojiIconEngine::key() const 0099 { 0100 return u"org.kde.KCountryFlagEmojiIconEngine"_s; 0101 } 0102 0103 void KCountryFlagEmojiIconEngine::paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) 0104 { 0105 // Not supported 0106 Q_UNUSED(mode); 0107 Q_UNUSED(state); 0108 0109 QFont font(*s_globalDefaultFont, painter->device()); 0110 font.setPixelSize(qMax(rect.width(), rect.height())); 0111 font.setFixedPitch(true); 0112 0113 QFontMetricsF metrics(font, painter->device()); 0114 QRectF tightRect = metrics.tightBoundingRect(d->m_emoji); 0115 while (tightRect.width() > rect.width() || tightRect.height() > rect.height()) { 0116 const auto widthDelta = std::floor(tightRect.width() - rect.width()); 0117 const auto heightDelta = std::floor(tightRect.height() - rect.height()); 0118 auto delta = std::max(std::max(1.0, widthDelta), std::max(1.0, heightDelta)); 0119 if (delta >= font.pixelSize()) { 0120 // when the delta is too large we'll chop the pixel size in half and hope the delta comes within a more reasonable range the next loop run 0121 static constexpr auto halfSize = 2; 0122 delta = std::floor(font.pixelSize() / halfSize); 0123 } 0124 font.setPixelSize(std::floor(font.pixelSize() - delta)); 0125 metrics = QFontMetricsF(font, painter->device()); 0126 tightRect = metrics.tightBoundingRect(d->m_emoji); 0127 } 0128 0129 const QRectF flagBoundingRect = metrics.boundingRect(rect, Qt::AlignCenter, d->m_emoji); 0130 0131 painter->setPen(qGuiApp->palette().color(QPalette::WindowText)); // in case we render the letters in absence of a flag 0132 // Confusingly the pixelSize for drawing must actually be without DPR but the rect calculation above 0133 // seems to be correct even with DPR in the pixelSize. 0134 font.setPixelSize(std::floor(font.pixelSize() / painter->device()->devicePixelRatioF())); 0135 painter->setFont(font); 0136 painter->drawText(flagBoundingRect, d->m_emoji); 0137 } 0138 0139 QPixmap KCountryFlagEmojiIconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) 0140 { 0141 return scaledPixmap(size, mode, state, 1.0); 0142 } 0143 0144 QPixmap KCountryFlagEmojiIconEngine::scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) 0145 { 0146 QPixmap pixmap(size); 0147 pixmap.setDevicePixelRatio(scale); 0148 pixmap.fill(Qt::transparent); 0149 { 0150 QPainter p(&pixmap); 0151 paint(&p, QRect(QPoint(0, 0), size), mode, state); 0152 } 0153 return pixmap; 0154 } 0155 0156 bool KCountryFlagEmojiIconEngine::isNull() 0157 { 0158 return d->m_emoji.isEmpty(); 0159 } 0160 0161 void KCountryFlagEmojiIconEngine::setGlobalDefaultFont(const QFont &font) 0162 { 0163 QFont swapable(font); 0164 s_globalDefaultFont->swap(swapable); 0165 }