1 // This file is part of Desktop App Toolkit,
2 // a set of libraries for developing nice desktop applications.
3 //
4 // For license and copyright information please follow this link:
5 // https://github.com/desktop-app/legal/blob/master/LEGAL
6 //
7 #include "ui/style/style_core_font.h"
8
9 #include "base/algorithm.h"
10 #include "base/debug_log.h"
11 #include "base/base_file_utilities.h"
12 #include "ui/style/style_core_custom_font.h"
13 #include "ui/integration.h"
14
15 #include <QtCore/QMap>
16 #include <QtCore/QVector>
17 #include <QtGui/QFontInfo>
18 #include <QtGui/QFontDatabase>
19 #include <QtWidgets/QApplication>
20
style_InitFontsResource()21 void style_InitFontsResource() {
22 #ifdef Q_OS_MAC // Use resources from the .app bundle on macOS.
23
24 base::RegisterBundledResources(u"lib_ui.rcc"_q);
25
26 #else // Q_OS_MAC
27
28 #ifndef DESKTOP_APP_USE_PACKAGED_FONTS
29 Q_INIT_RESOURCE(fonts);
30 #endif // !DESKTOP_APP_USE_PACKAGED_FONTS
31 #ifdef Q_OS_WIN
32 Q_INIT_RESOURCE(win);
33 #elif defined Q_OS_UNIX && !defined DESKTOP_APP_USE_PACKAGED // Q_OS_WIN
34 Q_INIT_RESOURCE(linux);
35 #endif // Q_OS_WIN || (Q_OS_UNIX && !DESKTOP_APP_USE_PACKAGED)
36
37 #endif // Q_OS_MAC
38 }
39
40 namespace style {
41 namespace internal {
42 namespace {
43
44 QMap<QString, int> fontFamilyMap;
45 QVector<QString> fontFamilies;
46 QMap<uint32, FontData*> fontsMap;
47
fontKey(int size,uint32 flags,int family)48 uint32 fontKey(int size, uint32 flags, int family) {
49 return (((uint32(family) << 12) | uint32(size)) << 6) | flags;
50 }
51
ValidateFont(const QString & familyName,int flags=0)52 bool ValidateFont(const QString &familyName, int flags = 0) {
53 QFont checkFont(familyName);
54 checkFont.setBold(flags & style::internal::FontBold);
55 checkFont.setItalic(flags & style::internal::FontItalic);
56 checkFont.setUnderline(flags & style::internal::FontUnderline);
57 auto realFamily = QFontInfo(checkFont).family();
58 if (realFamily.trimmed().compare(familyName, Qt::CaseInsensitive)) {
59 LOG(("Font Error: could not resolve '%1' font, got '%2'.").arg(familyName, realFamily));
60 return false;
61 }
62
63 auto metrics = QFontMetrics(checkFont);
64 if (!metrics.height()) {
65 LOG(("Font Error: got a zero height in '%1'.").arg(familyName));
66 return false;
67 }
68
69 return true;
70 }
71
LoadCustomFont(const QString & filePath,const QString & familyName,int flags=0)72 bool LoadCustomFont(const QString &filePath, const QString &familyName, int flags = 0) {
73 auto regularId = QFontDatabase::addApplicationFont(filePath);
74 if (regularId < 0) {
75 LOG(("Font Error: could not add '%1'.").arg(filePath));
76 return false;
77 }
78
79 const auto found = [&] {
80 for (auto &family : QFontDatabase::applicationFontFamilies(regularId)) {
81 LOG(("Font: from '%1' loaded '%2'").arg(filePath, family));
82 if (!family.trimmed().compare(familyName, Qt::CaseInsensitive)) {
83 return true;
84 }
85 }
86 return false;
87 }();
88 if (!found) {
89 LOG(("Font Error: could not locate '%1' font in '%2'.").arg(familyName, filePath));
90 return false;
91 }
92
93 return ValidateFont(familyName, flags);
94 }
95
SystemMonospaceFont()96 [[nodiscard]] QString SystemMonospaceFont() {
97 const auto type = QFontDatabase::FixedFont;
98 return QFontDatabase::systemFont(type).family();
99 }
100
ManualMonospaceFont()101 [[nodiscard]] QString ManualMonospaceFont() {
102 const auto kTryFirst = std::initializer_list<QString>{
103 "Consolas",
104 "Liberation Mono",
105 "Menlo",
106 "Courier"
107 };
108 for (const auto &family : kTryFirst) {
109 const auto resolved = QFontInfo(QFont(family)).family();
110 if (!resolved.trimmed().compare(family, Qt::CaseInsensitive)) {
111 return family;
112 }
113 }
114 return QString();
115 }
116
117 enum {
118 FontTypeRegular = 0,
119 FontTypeRegularItalic,
120 FontTypeBold,
121 FontTypeBoldItalic,
122 FontTypeSemibold,
123 FontTypeSemiboldItalic,
124
125 FontTypesCount,
126 };
127 #ifndef DESKTOP_APP_USE_PACKAGED_FONTS
128 QString FontTypeFiles[FontTypesCount] = {
129 "DAOpenSansRegular",
130 "DAOpenSansRegularItalic",
131 "DAOpenSansSemiboldAsBold",
132 "DAOpenSansSemiboldItalicAsBold",
133 "DAOpenSansSemiboldAsBold",
134 "DAOpenSansSemiboldItalicAsBold",
135 };
136 QString FontTypeNames[FontTypesCount] = {
137 "DAOpenSansRegular",
138 "DAOpenSansRegularItalic",
139 "DAOpenSansSemibold",
140 "DAOpenSansSemiboldItalic",
141 "DAOpenSansSemibold",
142 "DAOpenSansSemiboldItalic",
143 };
144 QString FontTypePersianFallbackFiles[FontTypesCount] = {
145 "DAVazirRegular",
146 "DAVazirRegular",
147 "DAVazirMediumAsBold",
148 "DAVazirMediumAsBold",
149 "DAVazirMediumAsBold",
150 "DAVazirMediumAsBold",
151 };
152 QString FontTypePersianFallback[FontTypesCount] = {
153 "DAVazirRegular",
154 "DAVazirRegular",
155 "DAVazirMedium",
156 "DAVazirMedium",
157 "DAVazirMedium",
158 "DAVazirMedium",
159 };
160 #endif // !DESKTOP_APP_USE_PACKAGED_FONTS
161 int32 FontTypeFlags[FontTypesCount] = {
162 0,
163 FontItalic,
164 FontBold,
165 FontBold | FontItalic,
166 FontSemibold,
167 FontSemibold | FontItalic,
168 };
169
170 bool Started = false;
171 QString Overrides[FontTypesCount];
172
173 } // namespace
174
StartFonts()175 void StartFonts() {
176 if (Started) {
177 return;
178 }
179 Started = true;
180
181 style_InitFontsResource();
182
183 #ifndef DESKTOP_APP_USE_PACKAGED_FONTS
184 [[maybe_unused]] bool areGood[FontTypesCount] = { false };
185 for (auto i = 0; i != FontTypesCount; ++i) {
186 const auto file = FontTypeFiles[i];
187 const auto name = FontTypeNames[i];
188 const auto flags = FontTypeFlags[i];
189 areGood[i] = LoadCustomFont(":/gui/fonts/" + file + ".ttf", name, flags);
190 Overrides[i] = name;
191
192 const auto persianFallbackFile = FontTypePersianFallbackFiles[i];
193 const auto persianFallback = FontTypePersianFallback[i];
194 LoadCustomFont(":/gui/fonts/" + persianFallbackFile + ".ttf", persianFallback, flags);
195
196 #ifdef Q_OS_WIN
197 // Attempt to workaround a strange font bug with Open Sans Semibold not loading.
198 // See https://github.com/telegramdesktop/tdesktop/issues/3276 for details.
199 // Crash happens on "options.maxh / _t->_st->font->height" with "division by zero".
200 // In that place "_t->_st->font" is "semiboldFont" is "font(13 "Open Sans Semibold").
201 const auto fallback = "Segoe UI";
202 if (!areGood[i]) {
203 if (ValidateFont(fallback, flags)) {
204 Overrides[i] = fallback;
205 LOG(("Fonts Info: Using '%1' instead of '%2'.").arg(fallback).arg(name));
206 }
207 }
208 // Disable default fallbacks to Segoe UI, see:
209 // https://github.com/telegramdesktop/tdesktop/issues/5368
210 //
211 //QFont::insertSubstitution(name, fallback);
212 #endif // Q_OS_WIN
213
214 QFont::insertSubstitution(name, persianFallback);
215 }
216
217 #ifdef Q_OS_MAC
218 auto list = QStringList();
219 list.append("STIXGeneral");
220 list.append(".SF NS Text");
221 list.append("Helvetica Neue");
222 list.append("Lucida Grande");
223 for (const auto &name : FontTypeNames) {
224 QFont::insertSubstitutions(name, list);
225 }
226 #endif // Q_OS_MAC
227 #endif // !DESKTOP_APP_USE_PACKAGED_FONTS
228
229 auto appFont = QApplication::font();
230 appFont.setStyleStrategy(QFont::PreferQuality);
231 QApplication::setFont(appFont);
232 }
233
GetPossibleEmptyOverride(int32 flags)234 QString GetPossibleEmptyOverride(int32 flags) {
235 flags = flags & (FontBold | FontSemibold | FontItalic);
236 int32 flagsBold = flags & (FontBold | FontItalic);
237 int32 flagsSemibold = flags & (FontSemibold | FontItalic);
238 if (flagsSemibold == (FontSemibold | FontItalic)) {
239 return Overrides[FontTypeSemiboldItalic];
240 } else if (flagsSemibold == FontSemibold) {
241 return Overrides[FontTypeSemibold];
242 } else if (flagsBold == (FontBold | FontItalic)) {
243 return Overrides[FontTypeBoldItalic];
244 } else if (flagsBold == FontBold) {
245 return Overrides[FontTypeBold];
246 } else if (flags == FontItalic) {
247 return Overrides[FontTypeRegularItalic];
248 } else if (flags == 0) {
249 return Overrides[FontTypeRegular];
250 }
251 return QString();
252 }
253
GetFontOverride(int32 flags)254 QString GetFontOverride(int32 flags) {
255 const auto result = GetPossibleEmptyOverride(flags);
256 return result.isEmpty() ? "Open Sans" : result;
257 }
258
MonospaceFont()259 QString MonospaceFont() {
260 static const auto family = [&]() -> QString {
261 const auto manual = ManualMonospaceFont();
262 const auto system = SystemMonospaceFont();
263
264 #if defined Q_OS_WIN || defined Q_OS_MAC
265 // Prefer our monospace font.
266 const auto useSystem = manual.isEmpty();
267 #else // Q_OS_WIN || Q_OS_MAC
268 // Prefer system monospace font.
269 const auto metrics = QFontMetrics(QFont(system));
270 const auto useSystem = manual.isEmpty()
271 || (metrics.horizontalAdvance(QChar('i')) == metrics.horizontalAdvance(QChar('W')));
272 #endif // Q_OS_WIN || Q_OS_MAC
273 return useSystem ? system : manual;
274 }();
275
276 return family;
277 }
278
destroyFonts()279 void destroyFonts() {
280 for (auto fontData : std::as_const(fontsMap)) {
281 delete fontData;
282 }
283 fontsMap.clear();
284 }
285
registerFontFamily(const QString & family)286 int registerFontFamily(const QString &family) {
287 auto result = fontFamilyMap.value(family, -1);
288 if (result < 0) {
289 result = fontFamilies.size();
290 fontFamilyMap.insert(family, result);
291 fontFamilies.push_back(family);
292 }
293 return result;
294 }
295
FontData(int size,uint32 flags,int family,Font * other)296 FontData::FontData(int size, uint32 flags, int family, Font *other)
297 : f(ResolveFont(flags, size))
298 , m(f)
299 , _size(size)
300 , _flags(flags)
301 , _family(family) {
302 if (other) {
303 memcpy(modified, other, sizeof(modified));
304 } else {
305 memset(modified, 0, sizeof(modified));
306 }
307 modified[_flags] = Font(this);
308
309 height = m.height();
310 ascent = m.ascent();
311 descent = m.descent();
312 spacew = width(QLatin1Char(' '));
313 elidew = width("...");
314 }
315
bold(bool set) const316 Font FontData::bold(bool set) const {
317 return otherFlagsFont(FontBold, set);
318 }
319
italic(bool set) const320 Font FontData::italic(bool set) const {
321 return otherFlagsFont(FontItalic, set);
322 }
323
underline(bool set) const324 Font FontData::underline(bool set) const {
325 return otherFlagsFont(FontUnderline, set);
326 }
327
strikeout(bool set) const328 Font FontData::strikeout(bool set) const {
329 return otherFlagsFont(FontStrikeOut, set);
330 }
331
semibold(bool set) const332 Font FontData::semibold(bool set) const {
333 return otherFlagsFont(FontSemibold, set);
334 }
335
monospace(bool set) const336 Font FontData::monospace(bool set) const {
337 return otherFlagsFont(FontMonospace, set);
338 }
339
size() const340 int FontData::size() const {
341 return _size;
342 }
343
flags() const344 uint32 FontData::flags() const {
345 return _flags;
346 }
347
family() const348 int FontData::family() const {
349 return _family;
350 }
351
otherFlagsFont(uint32 flag,bool set) const352 Font FontData::otherFlagsFont(uint32 flag, bool set) const {
353 int32 newFlags = set ? (_flags | flag) : (_flags & ~flag);
354 if (!modified[newFlags].v()) {
355 modified[newFlags] = Font(_size, newFlags, _family, modified);
356 }
357 return modified[newFlags];
358 }
359
Font(int size,uint32 flags,const QString & family)360 Font::Font(int size, uint32 flags, const QString &family) {
361 if (fontFamilyMap.isEmpty()) {
362 for (uint32 i = 0, s = fontFamilies.size(); i != s; ++i) {
363 fontFamilyMap.insert(fontFamilies.at(i), i);
364 }
365 }
366
367 auto i = fontFamilyMap.constFind(family);
368 if (i == fontFamilyMap.cend()) {
369 fontFamilies.push_back(family);
370 i = fontFamilyMap.insert(family, fontFamilies.size() - 1);
371 }
372 init(size, flags, i.value(), 0);
373 }
374
Font(int size,uint32 flags,int family)375 Font::Font(int size, uint32 flags, int family) {
376 init(size, flags, family, 0);
377 }
378
Font(int size,uint32 flags,int family,Font * modified)379 Font::Font(int size, uint32 flags, int family, Font *modified) {
380 init(size, flags, family, modified);
381 }
382
init(int size,uint32 flags,int family,Font * modified)383 void Font::init(int size, uint32 flags, int family, Font *modified) {
384 uint32 key = fontKey(size, flags, family);
385 auto i = fontsMap.constFind(key);
386 if (i == fontsMap.cend()) {
387 i = fontsMap.insert(key, new FontData(size, flags, family, modified));
388 }
389 ptr = i.value();
390 }
391
392 } // namespace internal
393 } // namespace style
394