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
12using namespace Qt::Literals::StringLiterals;
13
14namespace
15{
16
17Q_GLOBAL_STATIC(QFont, s_globalDefaultFont, "emoji"_L1)
18
19QString 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
43QString makeRegionEmoji(const QString &region)
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
73class Q_DECL_HIDDEN KCountryFlagEmojiIconEnginePrivate
74{
75public:
76 explicit KCountryFlagEmojiIconEnginePrivate(const QString &regionOrCountry)
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
86KCountryFlagEmojiIconEngine::KCountryFlagEmojiIconEngine(const QString &country)
87 : d(std::make_unique<KCountryFlagEmojiIconEnginePrivate>(args: country))
88{
89}
90
91KCountryFlagEmojiIconEngine::~KCountryFlagEmojiIconEngine() = default;
92
93QIconEngine *KCountryFlagEmojiIconEngine::clone() const
94{
95 return new KCountryFlagEmojiIconEngine(d->m_country);
96}
97
98QString KCountryFlagEmojiIconEngine::key() const
99{
100 return u"org.kde.KCountryFlagEmojiIconEngine"_s;
101}
102
103void 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
139QPixmap KCountryFlagEmojiIconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state)
140{
141 return scaledPixmap(size, mode, state, scale: 1.0);
142}
143
144QPixmap 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
156bool KCountryFlagEmojiIconEngine::isNull()
157{
158 return d->m_emoji.isEmpty();
159}
160
161void KCountryFlagEmojiIconEngine::setGlobalDefaultFont(const QFont &font)
162{
163 QFont swapable(font);
164 s_globalDefaultFont->swap(other&: swapable);
165}
166

source code of kguiaddons/src/util/kcountryflagemojiiconengine.cpp