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 &region)
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 &regionOrCountry)
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 }