OXYGENHUB
◆ main openmsx / src / console / OSDText.cc
10.4 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "OSDText.hh"
2
3
#include "CommandException.hh"
4
#include "Display.hh"
5
#include "FileContext.hh"
6
#include "FileOperations.hh"
7
#include "GLImage.hh"
8
#include "TTFFont.hh"
9
#include "TclObject.hh"
10
11
#include "StringOp.hh"
12
#include "join.hh"
13
#include "narrow.hh"
14
#include "stl.hh"
15
#include "unreachable.hh"
16
#include "utf8_core.hh"
17
18
#include <cassert>
19
#include <cmath>
20
#include <memory>
21
22
using namespace gl;
23
24
namespace openmsx {
25
26
OSDText::OSDText(Display& display_, const TclObject& name_)
27
	: OSDImageBasedWidget(display_, name_)
28
	, fontFile("skins/DejaVuSans.ttf.gz")
29
{
30
}
31
32
void OSDText::setProperty(
33
	Interpreter& interp, std::string_view propName, const TclObject& value)
34
{
35
	if (propName == "-text") {
36
		std::string_view val = value.getString();
37
		if (text != val) {
38
			text = val;
39
			// note: don't invalidate font (don't reopen font file)
40
			OSDImageBasedWidget::invalidateLocal();
41
			invalidateChildren();
42
		}
43
	} else if (propName == "-font") {
44
		std::string val(value.getString());
45
		if (fontFile != val) {
46
			if (std::string file = systemFileContext().resolve(val);
47
			    !FileOperations::isRegularFile(file)) {
48
				throw CommandException("Not a valid font file: ", val);
49
			}
50
			fontFile = val;
51
			invalidateRecursive();
52
		}
53
	} else if (propName == "-fontfaceindex") {
54
		int val = value.getInt(interp);
55
		if (val < 0) {
56
			throw CommandException("Not a valid value for -fontfaceindex, "
57
			                       "should be >= 0: ", val);
58
		}
59
		fontFaceIndex = val;
60
	} else if (propName == "-size") {
61
		int size2 = value.getInt(interp);
62
		if (size != size2) {
63
			size = size2;
64
			invalidateRecursive();
65
		}
66
	} else if (propName == "-wrap") {
67
		std::string_view val = value.getString();
68
		WrapMode wrapMode2 = [&] {
69
			if (val == "none") {
70
				return NONE;
71
			} else if (val == "word") {
72
				return WORD;
73
			} else if (val == "char") {
74
				return CHAR;
75
			} else {
76
				throw CommandException("Not a valid value for -wrap, "
77
					"expected one of 'none word char', but got '",
78
					val, "'.");
79
			}
80
		}();
81
		if (wrapMode != wrapMode2) {
82
			wrapMode = wrapMode2;
83
			invalidateRecursive();
84
		}
85
	} else if (propName == "-wrapw") {
86
		float wrapw2 = value.getFloat(interp);
87
		if (wrapw != wrapw2) {
88
			wrapw = wrapw2;
89
			invalidateRecursive();
90
		}
91
	} else if (propName == "-wraprelw") {
92
		float wraprelw2 = value.getFloat(interp);
93
		if (wraprelw != wraprelw2) {
94
			wraprelw = wraprelw2;
95
			invalidateRecursive();
96
		}
97
	} else {
98
		OSDImageBasedWidget::setProperty(interp, propName, value);
99
	}
100
}
101
102
void OSDText::getProperty(std::string_view propName, TclObject& result) const
103
{
104
	if (propName == "-text") {
105
		result = text;
106
	} else if (propName == "-font") {
107
		result = fontFile;
108
	} else if (propName == "-fontfaceindex") {
109
		result = fontFaceIndex;
110
	} else if (propName == "-size") {
111
		result = size;
112
	} else if (propName == "-wrap") {
113
		result = [&]{
114
			switch (wrapMode) {
115
				case NONE: return "none"sv;
116
				case WORD: return "word"sv;
117
				case CHAR: return "char"sv;
118
				default: UNREACHABLE;
119
			}
120
		}();
121
	} else if (propName == "-wrapw") {
122
		result = wrapw;
123
	} else if (propName == "-wraprelw") {
124
		result = wraprelw;
125
	} else {
126
		OSDImageBasedWidget::getProperty(propName, result);
127
	}
128
}
129
130
void OSDText::invalidateLocal()
131
{
132
	font = TTFFont(); // clear font
133
	OSDImageBasedWidget::invalidateLocal();
134
}
135
136
137
std::string_view OSDText::getType() const
138
{
139
	return "text";
140
}
141
142
vec2 OSDText::getSize(const OutputSurface& /*output*/) const
143
{
144
	if (image) {
145
		return vec2(image->getSize());
146
	} else {
147
		// we don't know the dimensions, must be because of an error
148
		assert(hasError());
149
		return {};
150
	}
151
}
152
153
uint8_t OSDText::getFadedAlpha() const
154
{
155
	return narrow_cast<uint8_t>(narrow_cast<float>(getRGBA(0) & 0xff) * getRecursiveFadeValue());
156
}
157
158
std::unique_ptr<GLImage> OSDText::create(OutputSurface& output)
159
{
160
	if (text.empty()) {
161
		return std::make_unique<GLImage>(ivec2(), 0);
162
	}
163
	int scale = getScaleFactor(output);
164
	if (font.empty()) {
165
		try {
166
			font = TTFFont(systemFileContext().resolve(fontFile),
167
			               size * scale, fontFaceIndex);
168
		} catch (MSXException& e) {
169
			throw MSXException("Couldn't open font: ", e.getMessage());
170
		}
171
	}
172
	try {
173
		vec2 pSize = getParent()->getSize(output);
174
		int maxWidth = narrow_cast<int>(lrintf(wrapw * narrow<float>(scale) + wraprelw * pSize.x));
175
		// Width can't be negative, if it is make it zero instead.
176
		// This will put each character on a different line.
177
		maxWidth = std::max(0, maxWidth);
178
179
		// TODO gradient???
180
		unsigned textRgba = getRGBA(0);
181
		std::string wrappedText;
182
		if (wrapMode == NONE) {
183
			wrappedText = text; // don't wrap
184
		} else if (wrapMode == WORD) {
185
			wrappedText = getWordWrappedText(text, maxWidth);
186
		} else if (wrapMode == CHAR) {
187
			wrappedText = getCharWrappedText(text, maxWidth);
188
		} else {
189
			UNREACHABLE;
190
		}
191
		// An alternative is to pass vector<string> to TTFFont::render().
192
		// That way we can avoid join() (in the wrap functions)
193
		// followed by // StringOp::split() (in TTFFont::render()).
194
		SDLSurfacePtr surface(font.render(wrappedText,
195
		                                  narrow_cast<uint8_t>(textRgba >> 24),
196
		                                  narrow_cast<uint8_t>(textRgba >> 16),
197
		                                  narrow_cast<uint8_t>(textRgba >>  8)));
198
		if (surface) {
199
			return std::make_unique<GLImage>(std::move(surface));
200
		} else {
201
			return std::make_unique<GLImage>(ivec2(), 0);
202
		}
203
	} catch (MSXException& e) {
204
		throw MSXException("Couldn't render text: ", e.getMessage());
205
	}
206
}
207
208
209
// Search for a position strictly between min and max which also points to the
210
// start of a (possibly multi-byte) utf8-character. If no such position exits,
211
// this function returns 'min'.
212
static constexpr size_t findCharSplitPoint(std::string_view line, size_t min, size_t max)
213
{
214
	auto pos = (min + max) / 2;
215
	auto beginIt = line.data();
216
	auto posIt = beginIt + pos;
217
218
	auto fwdIt = utf8::sync_forward(posIt);
219
	auto maxIt = beginIt + max;
220
	assert(fwdIt <= maxIt);
221
	if (fwdIt != maxIt) {
222
		return fwdIt - beginIt;
223
	}
224
225
	auto bwdIt = utf8::sync_backward(posIt);
226
	auto minIt = beginIt + min;
227
	assert(minIt <= bwdIt); (void)minIt;
228
	return bwdIt - beginIt;
229
}
230
231
// Search for a position that's strictly between min and max and which points
232
// to a character directly following a delimiter character. if no such position
233
// exits, this function returns 'min'.
234
// This function works correctly with multi-byte utf8-encoding as long as
235
// all delimiter characters are single byte chars.
236
static constexpr size_t findWordSplitPoint(std::string_view line, size_t min, size_t max)
237
{
238
	constexpr std::string_view delimiters = " -/";
239
240
	// initial guess for a good position
241
	assert(min < max);
242
	size_t pos = (min + max) / 2;
243
	if (pos == min) {
244
		// can't reduce further
245
		return min;
246
	}
247
248
	// try searching backward (this also checks current position)
249
	assert(pos > min);
250
	if (auto pos2 = line.substr(min, pos - min).find_last_of(delimiters);
251
	    pos2 != std::string_view::npos) {
252
		pos2 += min + 1;
253
		assert(min < pos2);
254
		assert(pos2 <= pos);
255
		return pos2;
256
	}
257
258
	// try searching forward
259
	if (auto pos2 = line.substr(pos, max - pos).find_first_of(delimiters);
260
	    pos2 != std::string_view::npos) {
261
		pos2 += pos;
262
		assert(pos2 < max);
263
		pos2 += 1; // char directly after a delimiter;
264
		if (pos2 < max) {
265
			return pos2;
266
		}
267
	}
268
269
	return min;
270
}
271
272
static constexpr size_t takeSingleChar(std::string_view /*line*/, unsigned /*maxWidth*/)
273
{
274
	return 1;
275
}
276
277
template<typename FindSplitPointFunc, typename CantSplitFunc>
278
size_t OSDText::split(const std::string& line, unsigned maxWidth,
279
                      FindSplitPointFunc findSplitPoint,
280
                      CantSplitFunc cantSplit,
281
                      bool removeTrailingSpaces) const
282
{
283
	if (line.empty()) {
284
		// empty line always fits (explicitly handle this because
285
		// SDL_TTF can't handle empty strings)
286
		return 0;
287
	}
288
289
	if (unsigned width = font.getSize(line).x; width <= maxWidth) {
290
		// whole line fits
291
		return line.size();
292
	}
293
294
	// binary search till we found the largest initial substring that is
295
	// not wider than maxWidth
296
	size_t min = 0;
297
	size_t max = line.size();
298
	// invariant: line.substr(0, min) DOES     fit
299
	//            line.substr(0, max) DOES NOT fit
300
	size_t cur = findSplitPoint(line, min, max);
301
	if (cur == 0) {
302
		// Could not find a valid split point, then split on char
303
		// (this also handles the case of a single too wide char)
304
		return cantSplit(line, maxWidth);
305
	}
306
	while (true) {
307
		assert(min < cur);
308
		assert(cur < max);
309
		std::string curStr = line.substr(0, cur);
310
		if (removeTrailingSpaces) {
311
			StringOp::trimRight(curStr, ' ');
312
		}
313
		unsigned width2 = font.getSize(curStr).x;
314
		if (width2 <= maxWidth) {
315
			// still fits, try to enlarge
316
			size_t next = findSplitPoint(line, cur, max);
317
			if (next == cur) {
318
				return cur;
319
			}
320
			min = cur;
321
			cur = next;
322
		} else {
323
			// doesn't fit anymore, try to shrink
324
			size_t next = findSplitPoint(line, min, cur);
325
			if (next == min) {
326
				if (min == 0) {
327
					// even the first word does not fit,
328
					// split on char (see above)
329
					return cantSplit(line, maxWidth);
330
				}
331
				return min;
332
			}
333
			max = cur;
334
			cur = next;
335
		}
336
	}
337
}
338
339
size_t OSDText::splitAtChar(const std::string& line, unsigned maxWidth) const
340
{
341
	return split(line, maxWidth, findCharSplitPoint, takeSingleChar, false);
342
}
343
344
struct SplitAtChar {
345
	explicit SplitAtChar(const OSDText& osdText_) : osdText(osdText_) {}
346
	[[nodiscard]] size_t operator()(const std::string& line, unsigned maxWidth) const {
347
		return osdText.splitAtChar(line, maxWidth);
348
	}
349
	const OSDText& osdText;
350
};
351
size_t OSDText::splitAtWord(const std::string& line, unsigned maxWidth) const
352
{
353
	return split(line, maxWidth, findWordSplitPoint, SplitAtChar(*this), true);
354
}
355
356
std::string OSDText::getCharWrappedText(const std::string& txt, unsigned maxWidth) const
357
{
358
	std::vector<std::string_view> wrappedLines;
359
	for (auto line : StringOp::split_view(txt, '\n')) {
360
		do {
361
			auto p = splitAtChar(std::string(line), maxWidth);
362
			wrappedLines.push_back(line.substr(0, p));
363
			line = line.substr(p);
364
		} while (!line.empty());
365
	}
366
	return join(wrappedLines, '\n');
367
}
368
369
std::string OSDText::getWordWrappedText(const std::string& txt, unsigned maxWidth) const
370
{
371
	std::vector<std::string_view> wrappedLines;
372
	for (auto line : StringOp::split_view(txt, '\n')) {
373
		do {
374
			auto p = splitAtWord(std::string(line), maxWidth);
375
			std::string_view first = line.substr(0, p);
376
			StringOp::trimRight(first, ' '); // remove trailing spaces
377
			wrappedLines.push_back(first);
378
			line = line.substr(p);
379
			StringOp::trimLeft(line, ' '); // remove leading spaces
380
		} while (!line.empty());
381
	}
382
	return join(wrappedLines, '\n');
383
}
384
385
} // namespace openmsx