KGuiAddons

kcountryflagemojiiconengine.cpp
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 QStringList emojiList;
34 emojiList.reserve(country.size());
35 for (const auto &c : country) {
36 emojiList.append(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(QChar(0x200b)); // U+200B Zero-Width Space
43 }
44
45 return emojiList.join(QString());
46}
47
48QString makeRegionEmoji(const QString &region)
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('-'_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(emoji.size() + 2 * hyphenlessRegion.size() + 2);
68 for (const auto &c : hyphenlessRegion) {
69 emoji.append(QChar(surrogatePairCodePoint));
70 emoji.append(QChar(basePoint + c.toLower().unicode()));
71 }
72 static const auto cancelTag = QString().append(QChar(surrogatePairCodePoint)).append(QChar(0xDC7F));
73 return emoji.append(cancelTag);
74}
75
76} // namespace
77
78class Q_DECL_HIDDEN KCountryFlagEmojiIconEnginePrivate
79{
80public:
81 explicit KCountryFlagEmojiIconEnginePrivate(const QString &regionOrCountry)
82 : m_country(regionOrCountry)
83 , m_emoji(regionOrCountry.contains("-"_L1) ? makeRegionEmoji(regionOrCountry) : makeCountryEmoji(regionOrCountry))
84 {
85 }
86
87 const QString m_country;
88 const QString m_emoji;
89};
90
92 : d(std::make_unique<KCountryFlagEmojiIconEnginePrivate>(country))
93{
94}
95
96KCountryFlagEmojiIconEngine::~KCountryFlagEmojiIconEngine() = default;
97
98QIconEngine *KCountryFlagEmojiIconEngine::clone() const
99{
100 return new KCountryFlagEmojiIconEngine(d->m_country);
101}
102
103QString KCountryFlagEmojiIconEngine::key() const
104{
105 return u"org.kde.KCountryFlagEmojiIconEngine"_s;
106}
107
108void 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(rect.width(), rect.height()));
116 font.setFixedPitch(true);
117
118 QFontMetricsF metrics(font, painter->device());
119 QRectF tightRect = metrics.tightBoundingRect(d->m_emoji);
120
121 if (tightRect.width() > rect.width() || tightRect.height() > rect.height()) {
122 const auto ratio = std::max({1.0, tightRect.width() / rect.width(), tightRect.height() / rect.height()});
123 font.setPixelSize(std::max(1.0, std::floor(font.pixelSize() / ratio)));
124 metrics = QFontMetricsF(font, painter->device());
125 tightRect = metrics.tightBoundingRect(d->m_emoji);
126 }
127
128 painter->setPen(qGuiApp->palette().color(QPalette::WindowText)); // in case we render the letters in absence of a flag
129
130 QRectF flagBoundingRect = metrics.boundingRect(rect, Qt::AlignCenter, 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(font.pixelSize() / dpr));
135 // The offset of the bounding rect needs to be also adjusted by the DPR
136 flagBoundingRect.moveTopLeft(flagBoundingRect.topLeft() / dpr);
137
138 painter->setFont(font);
139 painter->drawText(flagBoundingRect, d->m_emoji);
140}
141
142QPixmap KCountryFlagEmojiIconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state)
143{
144 return scaledPixmap(size, mode, state, 1.0);
145}
146
147QPixmap 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(Qt::transparent);
152 {
153 QPainter p(&pixmap);
154 paint(&p, QRect(QPoint(0, 0), size), mode, state);
155 }
156 return pixmap;
157}
158
160{
161 return d->m_emoji.isEmpty();
162}
163
165{
166 QFont swapable(font);
167 s_globalDefaultFont->swap(swapable);
168}
bool isNull() override
Check whether the internal emoji unicode sequence is null This does not necessarily mean that the pix...
static void setGlobalDefaultFont(const QFont &font)
Set the Global Default Font object This is primarily useful for platform themes that wish to force a ...
KCountryFlagEmojiIconEngine(const QString &regionOrCountry)
Construct a new KCountryFlagEmojiIconEngine object Please note that regional flag support can be spot...
KI18NLOCALEDATA_EXPORT KCountry country(const char *ianaId)
void append(QList< T > &&value)
void reserve(qsizetype size)
qreal devicePixelRatioF() const const
QPaintDevice * device() const const
void drawText(const QPoint &position, const QString &text)
void setFont(const QFont &font)
void setPen(Qt::PenStyle style)
void fill(const QColor &color)
void setDevicePixelRatio(qreal scaleFactor)
int height() const const
int width() const const
qreal height() const const
void moveTopLeft(const QPointF &position)
QPointF topLeft() const const
qreal width() const const
QString & append(QChar ch)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
const QChar * unicode() const const
QString join(QChar separator) const const
AlignCenter
transparent
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:09:48 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.