| 1 | // SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
| 2 | // SPDX-FileCopyrightText: 2022-2023 Harald Sitter <sitter@kde.org> |
| 3 | |
| 4 | #include "kcountryflagemojiiconengine.h" |
| 5 | |
| 6 | #include <QDebug> |
| 7 | #include <QFont> |
| 8 | #include <QGuiApplication> |
| 9 | #include <QPainter> |
| 10 | #include <QPalette> |
| 11 | |
| 12 | using namespace Qt::Literals::StringLiterals; |
| 13 | |
| 14 | namespace |
| 15 | { |
| 16 | |
| 17 | Q_GLOBAL_STATIC(QFont, s_globalDefaultFont, "emoji"_L1 ) |
| 18 | |
| 19 | QString makeCountryEmoji(const QString &country) |
| 20 | { |
| 21 | // The way this was set up by unicode is actually pretty smart. Country flags are based on their two character |
| 22 | // country codes within a given range of code points. And even better, the offset inside the range is the same |
| 23 | // as the offset inside ASCII. Meaning the offset of 'A' from 0 is the same as the offset of π¦ in the |
| 24 | // flag codepoint range. The way a flag is then denoted is e.g. <SURROGATEPAIR>π¦<SURROGATEPAIR>πΉ resulting in |
| 25 | // the Austrian flag. |
| 26 | // https://en.wikipedia.org/wiki/Regional_indicator_symbol |
| 27 | |
| 28 | static constexpr auto surrogatePairCodePoint = 0xD83C; // U+D83C |
| 29 | static constexpr auto flagCodePointStart = 0xDDE6; // U+1F1E6 (π¦) - NB: we are in UTF-16 |
| 30 | static constexpr auto offsetCodePointA = 'A'_L1.unicode(); // offset from 0, the flag code points have the same offsets |
| 31 | static constexpr auto basePoint = flagCodePointStart - offsetCodePointA; |
| 32 | |
| 33 | QStringList emojiList; |
| 34 | emojiList.reserve(asize: country.size()); |
| 35 | for (const auto &c : country) { |
| 36 | emojiList.append(t: QChar(surrogatePairCodePoint) + QChar(basePoint + c.toUpper().unicode())); |
| 37 | } |
| 38 | |
| 39 | // Valid flag country codes have only 2 characters. |
| 40 | // If we have more, separate the flag codepoints to avoid misrepresentations |
| 41 | if (country.size() != 2) { |
| 42 | return emojiList.join(sep: QChar(0x200b)); // U+200B Zero-Width Space |
| 43 | } |
| 44 | |
| 45 | return emojiList.join(sep: QString()); |
| 46 | } |
| 47 | |
| 48 | QString makeRegionEmoji(const QString ®ion) |
| 49 | { |
| 50 | // Region flags work much the same as country flags but with a slightly different format in a slightly different |
| 51 | // code point region. Specifically they use ISO 3166-2 as input (e.g. GB-SCT for Scotland). It all happens in |
| 52 | // the Unicode Block βTagsβ (starting at U+E0000) wherein it functions the same as the country codes do in their |
| 53 | // block. The offsets inside the block are the same as the ascii offsets and the emoji is constructed by combining |
| 54 | // the off set code points of the incoming region tag. They are prefixed with U+1F3F4 π΄ WAVING BLACK FLAG |
| 55 | // and suffixed with U+E007F CANCEL TAG. |
| 56 | // https://en.wikipedia.org/wiki/Regional_indicator_symbol |
| 57 | |
| 58 | auto hyphenlessRegion = region; |
| 59 | hyphenlessRegion.remove(c: '-'_L1); |
| 60 | |
| 61 | static constexpr auto surrogatePairCodePoint = 0xdb40; // U+DB40 |
| 62 | static constexpr auto flagCodePointStart = 0xDC41; // U+E0041 (Tag Latin Capital Letter A) - NB: we are in UTF-16 |
| 63 | static constexpr auto offsetCodePointA = 'A'_L1.unicode(); // offset from 0, the flag code points have the same offsets |
| 64 | static constexpr auto basePoint = flagCodePointStart - offsetCodePointA; |
| 65 | |
| 66 | auto emoji = u"π΄"_s ; |
| 67 | emoji.reserve(asize: emoji.size() + 2 * hyphenlessRegion.size() + 2); |
| 68 | for (const auto &c : hyphenlessRegion) { |
| 69 | emoji.append(c: QChar(surrogatePairCodePoint)); |
| 70 | emoji.append(c: QChar(basePoint + c.toLower().unicode())); |
| 71 | } |
| 72 | static const auto cancelTag = QString().append(c: QChar(surrogatePairCodePoint)).append(c: QChar(0xDC7F)); |
| 73 | return emoji.append(s: cancelTag); |
| 74 | } |
| 75 | |
| 76 | } // namespace |
| 77 | |
| 78 | class Q_DECL_HIDDEN KCountryFlagEmojiIconEnginePrivate |
| 79 | { |
| 80 | public: |
| 81 | explicit KCountryFlagEmojiIconEnginePrivate(const QString ®ionOrCountry) |
| 82 | : m_country(regionOrCountry) |
| 83 | , m_emoji(regionOrCountry.contains(s: "-"_L1 ) ? makeRegionEmoji(region: regionOrCountry) : makeCountryEmoji(country: regionOrCountry)) |
| 84 | { |
| 85 | } |
| 86 | |
| 87 | const QString m_country; |
| 88 | const QString m_emoji; |
| 89 | }; |
| 90 | |
| 91 | KCountryFlagEmojiIconEngine::KCountryFlagEmojiIconEngine(const QString &country) |
| 92 | : d(std::make_unique<KCountryFlagEmojiIconEnginePrivate>(args: country)) |
| 93 | { |
| 94 | } |
| 95 | |
| 96 | KCountryFlagEmojiIconEngine::~KCountryFlagEmojiIconEngine() = default; |
| 97 | |
| 98 | QIconEngine *KCountryFlagEmojiIconEngine::clone() const |
| 99 | { |
| 100 | return new KCountryFlagEmojiIconEngine(d->m_country); |
| 101 | } |
| 102 | |
| 103 | QString KCountryFlagEmojiIconEngine::key() const |
| 104 | { |
| 105 | return u"org.kde.KCountryFlagEmojiIconEngine"_s ; |
| 106 | } |
| 107 | |
| 108 | void KCountryFlagEmojiIconEngine::paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) |
| 109 | { |
| 110 | // Not supported |
| 111 | Q_UNUSED(mode); |
| 112 | Q_UNUSED(state); |
| 113 | |
| 114 | QFont font(*s_globalDefaultFont, painter->device()); |
| 115 | font.setPixelSize(qMax(a: rect.width(), b: rect.height())); |
| 116 | font.setFixedPitch(true); |
| 117 | |
| 118 | QFontMetricsF metrics(font, painter->device()); |
| 119 | QRectF tightRect = metrics.tightBoundingRect(text: d->m_emoji); |
| 120 | |
| 121 | if (tightRect.width() > rect.width() || tightRect.height() > rect.height()) { |
| 122 | const auto ratio = std::max(l: {1.0, tightRect.width() / rect.width(), tightRect.height() / rect.height()}); |
| 123 | font.setPixelSize(std::max(a: 1.0, b: std::floor(x: font.pixelSize() / ratio))); |
| 124 | metrics = QFontMetricsF(font, painter->device()); |
| 125 | tightRect = metrics.tightBoundingRect(text: d->m_emoji); |
| 126 | } |
| 127 | |
| 128 | painter->setPen(qGuiApp->palette().color(cr: QPalette::WindowText)); // in case we render the letters in absence of a flag |
| 129 | |
| 130 | QRectF flagBoundingRect = metrics.boundingRect(r: rect, flags: Qt::AlignCenter, string: d->m_emoji); |
| 131 | // Confusingly the pixelSize for drawing must actually be without DPR but the rect calculation above |
| 132 | // seems to be correct even with DPR in the pixelSize. |
| 133 | const auto dpr = painter->device()->devicePixelRatioF(); |
| 134 | font.setPixelSize(std::floor(x: font.pixelSize() / dpr)); |
| 135 | // The offset of the bounding rect needs to be also adjusted by the DPR |
| 136 | flagBoundingRect.moveTopLeft(p: flagBoundingRect.topLeft() / dpr); |
| 137 | |
| 138 | painter->setFont(font); |
| 139 | painter->drawText(r: flagBoundingRect, text: d->m_emoji); |
| 140 | } |
| 141 | |
| 142 | QPixmap KCountryFlagEmojiIconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) |
| 143 | { |
| 144 | return scaledPixmap(size, mode, state, scale: 1.0); |
| 145 | } |
| 146 | |
| 147 | QPixmap KCountryFlagEmojiIconEngine::scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) |
| 148 | { |
| 149 | QPixmap pixmap(size); |
| 150 | pixmap.setDevicePixelRatio(scale); |
| 151 | pixmap.fill(fillColor: Qt::transparent); |
| 152 | { |
| 153 | QPainter p(&pixmap); |
| 154 | paint(painter: &p, rect: QRect(QPoint(0, 0), size), mode, state); |
| 155 | } |
| 156 | return pixmap; |
| 157 | } |
| 158 | |
| 159 | bool KCountryFlagEmojiIconEngine::isNull() |
| 160 | { |
| 161 | return d->m_emoji.isEmpty(); |
| 162 | } |
| 163 | |
| 164 | void KCountryFlagEmojiIconEngine::setGlobalDefaultFont(const QFont &font) |
| 165 | { |
| 166 | QFont swapable(font); |
| 167 | s_globalDefaultFont->swap(other&: swapable); |
| 168 | } |
| 169 | |