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 | |