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