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 (textRgba >> 24) & 0xff, (textRgba >> 16) & 0xff, (textRgba >> 8) & 0xff));
191 if (surface) {
192 return std::make_unique<IMAGE>(output, std::move(surface));
193 } else {
194 return std::make_unique<IMAGE>(output, ivec2(), 0);
195 }
196 } catch (MSXException& e) {
197 throw MSXException("Couldn't render text: ", e.getMessage());
198 }
199}
200
201
202// Search for a position strictly between min and max which also points to the
203// start of a (possibly multi-byte) utf8-character. If no such position exits,
204// this function returns 'min'.
205static constexpr size_t findCharSplitPoint(string_view line, size_t min, size_t max)
206{
207 auto pos = (min + max) / 2;
208 auto beginIt = line.data();
209 auto posIt = beginIt + pos;
210
211 auto fwdIt = utf8::sync_forward(posIt);
212 auto maxIt = beginIt + max;
213 assert(fwdIt <= maxIt);
214 if (fwdIt != maxIt) {
215 return fwdIt - beginIt;
216 }
217
218 auto bwdIt = utf8::sync_backward(posIt);
219 auto minIt = beginIt + min;
220 assert(minIt <= bwdIt); (void)minIt;
221 return bwdIt - beginIt;
222}
223
224// Search for a position that's strictly between min and max and which points
225// to a character directly following a delimiter character. if no such position
226// exits, this function returns 'min'.
227// This function works correctly with multi-byte utf8-encoding as long as
228// all delimiter characters are single byte chars.
229static constexpr size_t findWordSplitPoint(string_view line, size_t min, size_t max)
230{
231 constexpr const char* const delimiters = " -/";
232
233 // initial guess for a good position
234 assert(min < max);
235 size_t pos = (min + max) / 2;
236 if (pos == min) {
237 // can't reduce further
238 return min;
239 }
240
241 // try searching backward (this also checks current position)
242 assert(pos > min);
243 if (auto pos2 = line.substr(min, pos - min).find_last_of(delimiters);
244 pos2 != string_view::npos) {
245 pos2 += min + 1;
246 assert(min < pos2);
247 assert(pos2 <= pos);
248 return pos2;
249 }
250
251 // try searching forward
252 if (auto pos2 = line.substr(pos, max - pos).find_first_of(delimiters);
253 pos2 != string_view::npos) {
254 pos2 += pos;
255 assert(pos2 < max);
256 pos2 += 1; // char directly after a delimiter;
257 if (pos2 < max) {
258 return pos2;
259 }
260 }
261
262 return min;
263}
264
265static constexpr size_t takeSingleChar(string_view /*line*/, unsigned /*maxWidth*/)
266{
267 return 1;
268}
269
270template<typename FindSplitPointFunc, typename CantSplitFunc>
271size_t OSDText::split(const string& line, unsigned maxWidth,
272 FindSplitPointFunc findSplitPoint,
273 CantSplitFunc cantSplit,
274 bool removeTrailingSpaces) const
275{
276 if (line.empty()) {
277 // empty line always fits (explicitly handle this because
278 // SDL_TTF can't handle empty strings)
279 return 0;
280 }
281
282 unsigned width = font.getSize(line)[0];
283 if (width <= maxWidth) {
284 // whole line fits
285 return line.size();
286 }
287
288 // binary search till we found the largest initial substring that is
289 // not wider than maxWidth
290 size_t min = 0;
291 size_t max = line.size();
292 // invariant: line.substr(0, min) DOES fit
293 // line.substr(0, max) DOES NOT fit
294 size_t cur = findSplitPoint(line, min, max);
295 if (cur == 0) {
296 // Could not find a valid split point, then split on char
297 // (this also handles the case of a single too wide char)
298 return cantSplit(line, maxWidth);
299 }
300 while (true) {
301 assert(min < cur);
302 assert(cur < max);
303 string curStr = line.substr(0, cur);
304 if (removeTrailingSpaces) {
305 StringOp::trimRight(curStr, ' ');
306 }
307 unsigned width2 = font.getSize(curStr)[0];
308 if (width2 <= maxWidth) {
309 // still fits, try to enlarge
310 size_t next = findSplitPoint(line, cur, max);
311 if (next == cur) {
312 return cur;
313 }
314 min = cur;
315 cur = next;
316 } else {
317 // doesn't fit anymore, try to shrink
318 size_t next = findSplitPoint(line, min, cur);
319 if (next == min) {
320 if (min == 0) {
321 // even the first word does not fit,
322 // split on char (see above)
323 return cantSplit(line, maxWidth);
324 }
325 return min;
326 }
327 max = cur;
328 cur = next;
329 }
330 }
331}
332
333size_t OSDText::splitAtChar(const std::string& line, unsigned maxWidth) const
334{
335 return split(line, maxWidth, findCharSplitPoint, takeSingleChar, false);
336}
337
339 explicit SplitAtChar(const OSDText& osdText_) : osdText(osdText_) {}
340 size_t operator()(const string& line, unsigned maxWidth) {
341 return osdText.splitAtChar(line, maxWidth);
342 }
344};
345size_t OSDText::splitAtWord(const std::string& line, unsigned maxWidth) const
346{
347 return split(line, maxWidth, findWordSplitPoint, SplitAtChar(*this), true);
348}
349
350string OSDText::getCharWrappedText(const string& txt, unsigned maxWidth) const
351{
352 std::vector<string_view> wrappedLines;
353 for (auto line : StringOp::split_view(txt, '\n')) {
354 do {
355 auto p = splitAtChar(string(line), maxWidth);
356 wrappedLines.push_back(line.substr(0, p));
357 line = line.substr(p);
358 } while (!line.empty());
359 }
360 return join(wrappedLines, '\n');
361}
362
363string OSDText::getWordWrappedText(const string& txt, unsigned maxWidth) const
364{
365 std::vector<string_view> wrappedLines;
366 for (auto line : StringOp::split_view(txt, '\n')) {
367 do {
368 auto p = splitAtWord(string(line), maxWidth);
369 string_view first = line.substr(0, p);
370 StringOp::trimRight(first, ' '); // remove trailing spaces
371 wrappedLines.push_back(first);
372 line = line.substr(p);
373 StringOp::trimLeft(line, ' '); // remove leading spaces
374 } while (!line.empty());
375 }
376 return join(wrappedLines, '\n');
377}
378
379std::unique_ptr<BaseImage> OSDText::createSDL(OutputSurface& output)
380{
381 return create<SDLImage>(output);
382}
383
384std::unique_ptr<BaseImage> OSDText::createGL(OutputSurface& output)
385{
386#if COMPONENT_GL
387 return create<GLImage>(output);
388#else
389 (void)&output;
390 return nullptr;
391#endif
392}
393
394} // 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, byte r, byte g, byte 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:20
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:152
vecN< 2, float > vec2
Definition: gl_vec.hh:149
constexpr vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:266
constexpr vecN< N, T > max(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:284
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:339
const OSDText & osdText
Definition: OSDText.cc:343
size_t operator()(const string &line, unsigned maxWidth)
Definition: OSDText.cc:340
#define UNREACHABLE
Definition: unreachable.hh:38