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 | QString emoji; |
34 | emoji.reserve(asize: 2 * country.size()); |
35 | for (const auto &c : country) { |
36 | emoji.append(c: QChar(surrogatePairCodePoint)); |
37 | emoji.append(c: QChar(basePoint + c.toUpper().unicode())); |
38 | } |
39 | |
40 | return emoji; |
41 | } |
42 | |
43 | QString makeRegionEmoji(const QString ®ion) |
44 | { |
45 | // Region flags work much the same as country flags but with a slightly different format in a slightly different |
46 | // code point region. Specifically they use ISO 3166-2 as input (e.g. GB-SCT for Scotland). It all happens in |
47 | // the Unicode Block βTagsβ (starting at U+E0000) wherein it functions the same as the country codes do in their |
48 | // block. The offsets inside the block are the same as the ascii offsets and the emoji is constructed by combining |
49 | // the off set code points of the incoming region tag. They are prefixed with U+1F3F4 π΄ WAVING BLACK FLAG |
50 | // and suffixed with U+E007F CANCEL TAG. |
51 | // https://en.wikipedia.org/wiki/Regional_indicator_symbol |
52 | |
53 | auto hyphenlessRegion = region; |
54 | hyphenlessRegion.remove(c: '-'_L1); |
55 | |
56 | static constexpr auto surrogatePairCodePoint = 0xdb40; // U+DB40 |
57 | static constexpr auto flagCodePointStart = 0xDC41; // U+E0041 (Tag Latin Capital Letter A) - NB: we are in UTF-16 |
58 | static constexpr auto offsetCodePointA = 'A'_L1.unicode(); // offset from 0, the flag code points have the same offsets |
59 | static constexpr auto basePoint = flagCodePointStart - offsetCodePointA; |
60 | |
61 | auto emoji = u"π΄"_s ; |
62 | emoji.reserve(asize: emoji.size() + 2 * hyphenlessRegion.size() + 2); |
63 | for (const auto &c : hyphenlessRegion) { |
64 | emoji.append(c: QChar(surrogatePairCodePoint)); |
65 | emoji.append(c: QChar(basePoint + c.toLower().unicode())); |
66 | } |
67 | static const auto cancelTag = QString().append(c: QChar(surrogatePairCodePoint)).append(c: QChar(0xDC7F)); |
68 | return emoji.append(s: cancelTag); |
69 | } |
70 | |
71 | } // namespace |
72 | |
73 | class Q_DECL_HIDDEN KCountryFlagEmojiIconEnginePrivate |
74 | { |
75 | public: |
76 | explicit KCountryFlagEmojiIconEnginePrivate(const QString ®ionOrCountry) |
77 | : m_country(regionOrCountry) |
78 | , m_emoji(regionOrCountry.contains(s: "-"_L1 ) ? makeRegionEmoji(region: regionOrCountry) : makeCountryEmoji(country: regionOrCountry)) |
79 | { |
80 | } |
81 | |
82 | const QString m_country; |
83 | const QString m_emoji; |
84 | }; |
85 | |
86 | KCountryFlagEmojiIconEngine::KCountryFlagEmojiIconEngine(const QString &country) |
87 | : d(std::make_unique<KCountryFlagEmojiIconEnginePrivate>(args: country)) |
88 | { |
89 | } |
90 | |
91 | KCountryFlagEmojiIconEngine::~KCountryFlagEmojiIconEngine() = default; |
92 | |
93 | QIconEngine *KCountryFlagEmojiIconEngine::clone() const |
94 | { |
95 | return new KCountryFlagEmojiIconEngine(d->m_country); |
96 | } |
97 | |
98 | QString KCountryFlagEmojiIconEngine::key() const |
99 | { |
100 | return u"org.kde.KCountryFlagEmojiIconEngine"_s ; |
101 | } |
102 | |
103 | void KCountryFlagEmojiIconEngine::paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) |
104 | { |
105 | // Not supported |
106 | Q_UNUSED(mode); |
107 | Q_UNUSED(state); |
108 | |
109 | QFont font(*s_globalDefaultFont, painter->device()); |
110 | font.setPixelSize(qMax(a: rect.width(), b: rect.height())); |
111 | font.setFixedPitch(true); |
112 | |
113 | QFontMetricsF metrics(font, painter->device()); |
114 | QRectF tightRect = metrics.tightBoundingRect(text: d->m_emoji); |
115 | while (tightRect.width() > rect.width() || tightRect.height() > rect.height()) { |
116 | const auto widthDelta = std::floor(x: tightRect.width() - rect.width()); |
117 | const auto heightDelta = std::floor(x: tightRect.height() - rect.height()); |
118 | auto delta = std::max(a: std::max(a: 1.0, b: widthDelta), b: std::max(a: 1.0, b: heightDelta)); |
119 | if (delta >= font.pixelSize()) { |
120 | // 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 |
121 | static constexpr auto halfSize = 2; |
122 | delta = std::floor(x: font.pixelSize() / halfSize); |
123 | } |
124 | font.setPixelSize(std::floor(x: font.pixelSize() - delta)); |
125 | metrics = QFontMetricsF(font, painter->device()); |
126 | tightRect = metrics.tightBoundingRect(text: d->m_emoji); |
127 | } |
128 | |
129 | const QRectF flagBoundingRect = metrics.boundingRect(r: rect, flags: Qt::AlignCenter, string: d->m_emoji); |
130 | |
131 | painter->setPen(qGuiApp->palette().color(cr: QPalette::WindowText)); // in case we render the letters in absence of a flag |
132 | // Confusingly the pixelSize for drawing must actually be without DPR but the rect calculation above |
133 | // seems to be correct even with DPR in the pixelSize. |
134 | font.setPixelSize(std::floor(x: font.pixelSize() / painter->device()->devicePixelRatioF())); |
135 | painter->setFont(font); |
136 | painter->drawText(r: flagBoundingRect, text: d->m_emoji); |
137 | } |
138 | |
139 | QPixmap KCountryFlagEmojiIconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) |
140 | { |
141 | return scaledPixmap(size, mode, state, scale: 1.0); |
142 | } |
143 | |
144 | QPixmap KCountryFlagEmojiIconEngine::scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) |
145 | { |
146 | QPixmap pixmap(size); |
147 | pixmap.setDevicePixelRatio(scale); |
148 | pixmap.fill(fillColor: Qt::transparent); |
149 | { |
150 | QPainter p(&pixmap); |
151 | paint(painter: &p, rect: QRect(QPoint(0, 0), size), mode, state); |
152 | } |
153 | return pixmap; |
154 | } |
155 | |
156 | bool KCountryFlagEmojiIconEngine::isNull() |
157 | { |
158 | return d->m_emoji.isEmpty(); |
159 | } |
160 | |
161 | void KCountryFlagEmojiIconEngine::setGlobalDefaultFont(const QFont &font) |
162 | { |
163 | QFont swapable(font); |
164 | s_globalDefaultFont->swap(other&: swapable); |
165 | } |
166 | |