// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL // SPDX-FileCopyrightText: 2022-2023 Harald Sitter #include "kcountryflagemojiiconengine.h" #include #include #include #include #include using namespace Qt::Literals::StringLiterals; namespace { Q_GLOBAL_STATIC(QFont, s_globalDefaultFont, "emoji"_L1) QString makeCountryEmoji(const QString &country) { // The way this was set up by unicode is actually pretty smart. Country flags are based on their two character // country codes within a given range of code points. And even better, the offset inside the range is the same // as the offset inside ASCII. Meaning the offset of 'A' from 0 is the same as the offset of πŸ‡¦ in the // flag codepoint range. The way a flag is then denoted is e.g. πŸ‡¦πŸ‡Ή resulting in // the Austrian flag. // https://en.wikipedia.org/wiki/Regional_indicator_symbol static constexpr auto surrogatePairCodePoint = 0xD83C; // U+D83C static constexpr auto flagCodePointStart = 0xDDE6; // U+1F1E6 (πŸ‡¦) - NB: we are in UTF-16 static constexpr auto offsetCodePointA = 'A'_L1.unicode(); // offset from 0, the flag code points have the same offsets static constexpr auto basePoint = flagCodePointStart - offsetCodePointA; QStringList emojiList; emojiList.reserve(country.size()); for (const auto &c : country) { emojiList.append(QChar(surrogatePairCodePoint) + QChar(basePoint + c.toUpper().unicode())); } // Valid flag country codes have only 2 characters. // If we have more, separate the flag codepoints to avoid misrepresentations if (country.size() != 2) { return emojiList.join(QChar(0x200b)); // U+200B Zero-Width Space } return emojiList.join(QString()); } QString makeRegionEmoji(const QString ®ion) { // Region flags work much the same as country flags but with a slightly different format in a slightly different // code point region. Specifically they use ISO 3166-2 as input (e.g. GB-SCT for Scotland). It all happens in // the Unicode Block β€œTags” (starting at U+E0000) wherein it functions the same as the country codes do in their // block. The offsets inside the block are the same as the ascii offsets and the emoji is constructed by combining // the off set code points of the incoming region tag. They are prefixed with U+1F3F4 🏴 WAVING BLACK FLAG // and suffixed with U+E007F CANCEL TAG. // https://en.wikipedia.org/wiki/Regional_indicator_symbol auto hyphenlessRegion = region; hyphenlessRegion.remove('-'_L1); static constexpr auto surrogatePairCodePoint = 0xdb40; // U+DB40 static constexpr auto flagCodePointStart = 0xDC41; // U+E0041 (Tag Latin Capital Letter A) - NB: we are in UTF-16 static constexpr auto offsetCodePointA = 'A'_L1.unicode(); // offset from 0, the flag code points have the same offsets static constexpr auto basePoint = flagCodePointStart - offsetCodePointA; auto emoji = u"🏴"_s; emoji.reserve(emoji.size() + 2 * hyphenlessRegion.size() + 2); for (const auto &c : hyphenlessRegion) { emoji.append(QChar(surrogatePairCodePoint)); emoji.append(QChar(basePoint + c.toLower().unicode())); } static const auto cancelTag = QString().append(QChar(surrogatePairCodePoint)).append(QChar(0xDC7F)); return emoji.append(cancelTag); } } // namespace class Q_DECL_HIDDEN KCountryFlagEmojiIconEnginePrivate { public: explicit KCountryFlagEmojiIconEnginePrivate(const QString ®ionOrCountry) : m_country(regionOrCountry) , m_emoji(regionOrCountry.contains("-"_L1) ? makeRegionEmoji(regionOrCountry) : makeCountryEmoji(regionOrCountry)) { } const QString m_country; const QString m_emoji; }; KCountryFlagEmojiIconEngine::KCountryFlagEmojiIconEngine(const QString &country) : d(std::make_unique(country)) { } KCountryFlagEmojiIconEngine::~KCountryFlagEmojiIconEngine() = default; QIconEngine *KCountryFlagEmojiIconEngine::clone() const { return new KCountryFlagEmojiIconEngine(d->m_country); } QString KCountryFlagEmojiIconEngine::key() const { return u"org.kde.KCountryFlagEmojiIconEngine"_s; } void KCountryFlagEmojiIconEngine::paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) { // Not supported Q_UNUSED(mode); Q_UNUSED(state); QFont font(*s_globalDefaultFont, painter->device()); font.setPixelSize(qMax(rect.width(), rect.height())); font.setFixedPitch(true); QFontMetricsF metrics(font, painter->device()); QRectF tightRect = metrics.tightBoundingRect(d->m_emoji); if (tightRect.width() > rect.width() || tightRect.height() > rect.height()) { const auto ratio = std::max({1.0, tightRect.width() / rect.width(), tightRect.height() / rect.height()}); font.setPixelSize(std::max(1.0, std::floor(font.pixelSize() / ratio))); metrics = QFontMetricsF(font, painter->device()); tightRect = metrics.tightBoundingRect(d->m_emoji); } painter->setPen(qGuiApp->palette().color(QPalette::WindowText)); // in case we render the letters in absence of a flag QRectF flagBoundingRect = metrics.boundingRect(rect, Qt::AlignCenter, d->m_emoji); // Confusingly the pixelSize for drawing must actually be without DPR but the rect calculation above // seems to be correct even with DPR in the pixelSize. const auto dpr = painter->device()->devicePixelRatioF(); font.setPixelSize(std::floor(font.pixelSize() / dpr)); // The offset of the bounding rect needs to be also adjusted by the DPR flagBoundingRect.moveTopLeft(flagBoundingRect.topLeft() / dpr); painter->setFont(font); painter->drawText(flagBoundingRect, d->m_emoji); } QPixmap KCountryFlagEmojiIconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) { return scaledPixmap(size, mode, state, 1.0); } QPixmap KCountryFlagEmojiIconEngine::scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) { QPixmap pixmap(size); pixmap.setDevicePixelRatio(scale); pixmap.fill(Qt::transparent); { QPainter p(&pixmap); paint(&p, QRect(QPoint(0, 0), size), mode, state); } return pixmap; } bool KCountryFlagEmojiIconEngine::isNull() { return d->m_emoji.isEmpty(); } void KCountryFlagEmojiIconEngine::setGlobalDefaultFont(const QFont &font) { QFont swapable(font); s_globalDefaultFont->swap(swapable); }