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