OXYGENHUB
◆ main openmsx / src / console / TTFFont.cc
6.6 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "TTFFont.hh"
2
3
#include "LocalFileReference.hh"
4
#include "MSXException.hh"
5
6
#include "StringOp.hh"
7
#include "stl.hh"
8
#include "zstring_view.hh"
9
10
#include <SDL_ttf.h>
11
12
#include <algorithm>
13
#include <cassert>
14
#include <vector>
15
16
namespace openmsx {
17
18
class SDLTTF
19
{
20
public:
21
	SDLTTF(const SDLTTF&) = delete;
22
	SDLTTF(SDLTTF&&) = delete;
23
	SDLTTF& operator=(const SDLTTF&) = delete;
24
	SDLTTF& operator=(SDLTTF&&) = delete;
25
26
	static SDLTTF& instance();
27
28
private:
29
	SDLTTF();
30
	~SDLTTF();
31
};
32
33
class TTFFontPool
34
{
35
public:
36
	TTFFontPool(const TTFFontPool&) = delete;
37
	TTFFontPool(TTFFontPool&&) = delete;
38
	TTFFontPool& operator=(const TTFFontPool&) = delete;
39
	TTFFontPool& operator=(TTFFontPool&&) = delete;
40
41
	static TTFFontPool& instance();
42
	TTF_Font* get(const std::string& filename, int ptSize, int faceIndex);
43
	void release(TTF_Font* font);
44
45
private:
46
	TTFFontPool() = default;
47
	~TTFFontPool();
48
49
	// We want to keep the LocalFileReference object alive for as long as
50
	// the SDL_ttf library uses the font. This solves a problem we had in
51
	// the past on windows that temporary files were not cleaned up. The
52
	// scenario went like this:
53
	//  1. new LocalFileReference object (possibly) creates a temp file
54
	//  2. SDL_ttf opens this file (and keeps it open)
55
	//  3. LocalFileReference object goes out of scope and deletes the
56
	//     temp file
57
	//  4. (much later) we're done using the font and SDL_ttf closes the
58
	//     file
59
	// Step 3 goes wrong in windows because it's not possible to delete a
60
	// still opened file (no problem in unix). Solved by swapping the order
61
	// of step 3 and 4. Though this has the disadvantage that if openMSX
62
	// crashes between step 3 and 4 the temp file is still left behind.
63
	struct FontInfo {
64
		LocalFileReference file;
65
		TTF_Font* font;
66
		std::string name;
67
		int size;
68
		int count;
69
		int faceIndex;
70
	};
71
	std::vector<FontInfo> pool;
72
};
73
74
75
// class SDLTTF
76
77
SDLTTF::SDLTTF()
78
{
79
	if (TTF_Init() < 0) {
80
		throw FatalError("Couldn't initialize SDL_ttf: ", TTF_GetError());
81
	}
82
}
83
84
SDLTTF::~SDLTTF()
85
{
86
	TTF_Quit();
87
}
88
89
SDLTTF& SDLTTF::instance()
90
{
91
	static SDLTTF oneInstance;
92
	return oneInstance;
93
}
94
95
96
// class TTFFontPool
97
98
TTFFontPool::~TTFFontPool()
99
{
100
	assert(pool.empty());
101
}
102
103
TTFFontPool& TTFFontPool::instance()
104
{
105
	static TTFFontPool oneInstance;
106
	return oneInstance;
107
}
108
109
TTF_Font* TTFFontPool::get(const std::string& filename, int ptSize, int faceIndex)
110
{
111
	if (auto it = std::ranges::find(pool, std::tuple(filename, ptSize, faceIndex),
112
	        [](auto& info) { return std::tuple(info.name, info.size, info.faceIndex); });
113
	    it != end(pool)) {
114
		++it->count;
115
		return it->font;
116
	}
117
118
	SDLTTF::instance(); // init library
119
	FontInfo info;
120
	info.file = LocalFileReference(filename);
121
	auto* result = TTF_OpenFontIndex(info.file.getFilename().c_str(), ptSize, faceIndex);
122
	if (!result) {
123
		throw MSXException(TTF_GetError());
124
	}
125
	info.font = result;
126
	info.name = filename;
127
	info.size = ptSize;
128
	info.count = 1;
129
	info.faceIndex = faceIndex;
130
	pool.push_back(std::move(info));
131
	return result;
132
}
133
134
void TTFFontPool::release(TTF_Font* font)
135
{
136
	auto it = rfind_unguarded(pool, font, &FontInfo::font);
137
	--it->count;
138
	if (it->count == 0) {
139
		TTF_CloseFont(it->font);
140
		move_pop_back(pool, it);
141
	}
142
}
143
144
145
// class TTFFont
146
147
TTFFont::TTFFont(const std::string& filename, int ptSize, int faceIndex)
148
	: font(TTFFontPool::instance().get(filename, ptSize, faceIndex))
149
{
150
}
151
152
TTFFont::~TTFFont()
153
{
154
	if (!font) return;
155
	TTFFontPool::instance().release(static_cast<TTF_Font*>(font));
156
}
157
158
SDLSurfacePtr TTFFont::render(std::string text, uint8_t r, uint8_t g, uint8_t b) const
159
{
160
	SDL_Color color = { r, g, b, 0 };
161
162
	// Optimization: remove trailing empty lines
163
	StringOp::trimRight(text, " \n");
164
	if (text.empty()) return SDLSurfacePtr(nullptr);
165
166
	// Split on newlines
167
	auto lines_view = StringOp::split_view(text, '\n');
168
	auto lines_it = lines_view.begin();
169
	auto lines_end = lines_view.end();
170
	assert(lines_it != lines_end);
171
172
	auto current_line = *lines_it;
173
	++lines_it;
174
	if (lines_it == lines_end) {
175
		// Special case for a single line: we can avoid the
176
		// copy to an extra SDL_Surface
177
		assert(!text.empty());
178
		SDLSurfacePtr surface(
179
			TTF_RenderUTF8_Blended(static_cast<TTF_Font*>(font),
180
			                       text.c_str(), color));
181
		if (!surface) {
182
			throw MSXException(TTF_GetError());
183
		}
184
		return surface;
185
	}
186
187
	// Determine maximum width and lineHeight
188
	int width = 0;
189
	int lineHeight = 0; // initialize to avoid warning
190
	int numLines = 1;
191
	while (true) {
192
		auto [w, h] = getSize(std::string(current_line));
193
		width = std::max(width, w);
194
		lineHeight = h;
195
		if (lines_it == lines_end) break;
196
		current_line = *lines_it;
197
		++lines_it;
198
		++numLines;
199
	}
200
	// There might be extra space between two successive lines
201
	// (so lineSkip might be bigger than lineHeight).
202
	int lineSkip = getHeight();
203
	// We assume that height is the same for all lines.
204
	// For the last line we don't include spacing between two lines.
205
	auto height = (numLines - 1) * lineSkip + lineHeight;
206
207
	// Create destination surface (initial surface is fully transparent)
208
	SDLSurfacePtr destination(SDL_CreateRGBSurface(SDL_SWSURFACE, width, height,
209
			32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000));
210
	if (!destination) {
211
		throw MSXException("Couldn't allocate surface for multiline text.");
212
	}
213
214
	// Actually render the text:
215
	// TODO use enumerate() in the future (c++20)
216
	int i = -1;
217
	for (auto line : lines_view) {
218
		++i;
219
		// Render single line
220
		if (line.empty()) {
221
			// SDL_TTF gives an error on empty lines, but we can
222
			// simply skip such lines
223
			continue;
224
		}
225
		SDLSurfacePtr surf(TTF_RenderUTF8_Blended(
226
			static_cast<TTF_Font*>(font),
227
			std::string(line).c_str(), color));
228
		if (!surf) {
229
			throw MSXException(TTF_GetError());
230
		}
231
232
		// Copy line to destination surface
233
		SDL_Rect rect;
234
		rect.x = 0;
235
		rect.y = Sint16(i * lineSkip);
236
		SDL_SetSurfaceBlendMode(surf.get(), SDL_BLENDMODE_NONE); // no blending during copy
237
		SDL_BlitSurface(surf.get(), nullptr, destination.get(), &rect);
238
	}
239
	return destination;
240
}
241
242
int TTFFont::getHeight() const
243
{
244
	return TTF_FontLineSkip(static_cast<TTF_Font*>(font));
245
}
246
247
bool TTFFont::isFixedWidth() const
248
{
249
	return TTF_FontFaceIsFixedWidth(static_cast<TTF_Font*>(font)) != 0;
250
}
251
252
int TTFFont::getWidth() const
253
{
254
	int advance;
255
	if (TTF_GlyphMetrics(static_cast<TTF_Font*>(font), Uint16('M'),
256
	                     nullptr /*minx*/, nullptr /*maxx*/,
257
	                     nullptr /*miny*/, nullptr /*maxy*/,
258
	                     &advance)) {
259
		// error?
260
		return 10; //fallback-width
261
	}
262
	return advance;
263
}
264
265
gl::ivec2 TTFFont::getSize(zstring_view text) const
266
{
267
	int width, height;
268
	if (TTF_SizeUTF8(static_cast<TTF_Font*>(font), text.c_str(),
269
	                 &width, &height)) {
270
		throw MSXException(TTF_GetError());
271
	}
272
	return {width, height};
273
}
274
275
} // namespace openmsx