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