openMSX
OSDText.cc
Go to the documentation of this file.
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
22using std::string;
23using std::string_view;
24using namespace gl;
25
26namespace openmsx {
27
28OSDText::OSDText(Display& display_, const TclObject& name_)
29 : OSDImageBasedWidget(display_, name_)
30 , fontFile("skins/DejaVuSans.ttf.gz")
31{
32}
33
35 Interpreter& interp, string_view propName, const TclObject& value)
36{
37 if (propName == "-text") {
38 string_view val = value.getString();
39 if (text != val) {
40 text = val;
41 // note: don't invalidate font (don't reopen font file)
44 }
45 } else if (propName == "-font") {
46 string val(value.getString());
47 if (fontFile != val) {
48 if (string file = systemFileContext().resolve(val);
50 throw CommandException("Not a valid font file: ", val);
51 }
52 fontFile = val;
54 }
55 } else if (propName == "-size") {
56 int size2 = value.getInt(interp);
57 if (size != size2) {
58 size = size2;
60 }
61 } else if (propName == "-wrap") {
62 string_view val = value.getString();
63 WrapMode wrapMode2 = [&] {
64 if (val == "none") {
65 return NONE;
66 } else if (val == "word") {
67 return WORD;
68 } else if (val == "char") {
69 return CHAR;
70 } else {
71 throw CommandException("Not a valid value for -wrap, "
72 "expected one of 'none word char', but got '",
73 val, "'.");
74 }
75 }();
76 if (wrapMode != wrapMode2) {
77 wrapMode = wrapMode2;
79 }
80 } else if (propName == "-wrapw") {
81 float wrapw2 = value.getFloat(interp);
82 if (wrapw != wrapw2) {
83 wrapw = wrapw2;
85 }
86 } else if (propName == "-wraprelw") {
87 float wraprelw2 = value.getFloat(interp);
88 if (wraprelw != wraprelw2) {
89 wraprelw = wraprelw2;
91 }
92 } else {
93 OSDImageBasedWidget::setProperty(interp, propName, value);
94 }
95}
96
97void OSDText::getProperty(string_view propName, TclObject& result) const
98{
99 if (propName == "-text") {
100 result = text;
101 } else if (propName == "-font") {
102 result = fontFile;
103 } else if (propName == "-size") {
104 result = size;
105 } else if (propName == "-wrap") {
106 string wrapString;
107 switch (wrapMode) {
108 case NONE: wrapString = "none"; break;
109 case WORD: wrapString = "word"; break;
110 case CHAR: wrapString = "char"; break;
111 default: UNREACHABLE;
112 }
113 result = wrapString;
114 } else if (propName == "-wrapw") {
115 result = wrapw;
116 } else if (propName == "-wraprelw") {
117 result = wraprelw;
118 } else {
119 OSDImageBasedWidget::getProperty(propName, result);
120 }
121}
122
123void OSDText::invalidateLocal()
124{
125 font = TTFFont(); // clear font
127}
128
129
130string_view OSDText::getType() const
131{
132 return "text";
133}
134
135vec2 OSDText::getSize(const OutputSurface& /*output*/) const
136{
137 if (image) {
138 return vec2(image->getSize());
139 } else {
140 // we don't know the dimensions, must be because of an error
141 assert(hasError());
142 return {};
143 }
144}
145
146uint8_t OSDText::getFadedAlpha() const
147{
148 return narrow_cast<uint8_t>(narrow_cast<float>(getRGBA(0) & 0xff) * getRecursiveFadeValue());
149}
150
151std::unique_ptr<GLImage> OSDText::create(OutputSurface& output)
152{
153 if (text.empty()) {
154 return std::make_unique<GLImage>(ivec2(), 0);
155 }
156 int scale = getScaleFactor(output);
157 if (font.empty()) {
158 try {
159 font = TTFFont(systemFileContext().resolve(fontFile),
160 size * scale);
161 } catch (MSXException& e) {
162 throw MSXException("Couldn't open font: ", e.getMessage());
163 }
164 }
165 try {
166 vec2 pSize = getParent()->getSize(output);
167 int maxWidth = narrow_cast<int>(lrintf(wrapw * narrow<float>(scale) + wraprelw * pSize.x));
168 // Width can't be negative, if it is make it zero instead.
169 // This will put each character on a different line.
170 maxWidth = std::max(0, maxWidth);
171
172 // TODO gradient???
173 unsigned textRgba = getRGBA(0);
174 string wrappedText;
175 if (wrapMode == NONE) {
176 wrappedText = text; // don't wrap
177 } else if (wrapMode == WORD) {
178 wrappedText = getWordWrappedText(text, maxWidth);
179 } else if (wrapMode == CHAR) {
180 wrappedText = getCharWrappedText(text, maxWidth);
181 } else {
183 }
184 // An alternative is to pass vector<string> to TTFFont::render().
185 // That way we can avoid join() (in the wrap functions)
186 // followed by // StringOp::split() (in TTFFont::render()).
187 SDLSurfacePtr surface(font.render(wrappedText,
188 narrow_cast<uint8_t>(textRgba >> 24),
189 narrow_cast<uint8_t>(textRgba >> 16),
190 narrow_cast<uint8_t>(textRgba >> 8)));
191 if (surface) {
192 return std::make_unique<GLImage>(std::move(surface));
193 } else {
194 return std::make_unique<GLImage>(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 if (unsigned width = font.getSize(line).x; width <= maxWidth) {
283 // whole line fits
284 return line.size();
285 }
286
287 // binary search till we found the largest initial substring that is
288 // not wider than maxWidth
289 size_t min = 0;
290 size_t max = line.size();
291 // invariant: line.substr(0, min) DOES fit
292 // line.substr(0, max) DOES NOT fit
293 size_t cur = findSplitPoint(line, min, max);
294 if (cur == 0) {
295 // Could not find a valid split point, then split on char
296 // (this also handles the case of a single too wide char)
297 return cantSplit(line, maxWidth);
298 }
299 while (true) {
300 assert(min < cur);
301 assert(cur < max);
302 string curStr = line.substr(0, cur);
303 if (removeTrailingSpaces) {
304 StringOp::trimRight(curStr, ' ');
305 }
306 unsigned width2 = font.getSize(curStr).x;
307 if (width2 <= maxWidth) {
308 // still fits, try to enlarge
309 size_t next = findSplitPoint(line, cur, max);
310 if (next == cur) {
311 return cur;
312 }
313 min = cur;
314 cur = next;
315 } else {
316 // doesn't fit anymore, try to shrink
317 size_t next = findSplitPoint(line, min, cur);
318 if (next == min) {
319 if (min == 0) {
320 // even the first word does not fit,
321 // split on char (see above)
322 return cantSplit(line, maxWidth);
323 }
324 return min;
325 }
326 max = cur;
327 cur = next;
328 }
329 }
330}
331
332size_t OSDText::splitAtChar(const std::string& line, unsigned maxWidth) const
333{
334 return split(line, maxWidth, findCharSplitPoint, takeSingleChar, false);
335}
336
338 explicit SplitAtChar(const OSDText& osdText_) : osdText(osdText_) {}
339 [[nodiscard]] size_t operator()(const string& line, unsigned maxWidth) const {
340 return osdText.splitAtChar(line, maxWidth);
341 }
343};
344size_t OSDText::splitAtWord(const std::string& line, unsigned maxWidth) const
345{
346 return split(line, maxWidth, findWordSplitPoint, SplitAtChar(*this), true);
347}
348
349string OSDText::getCharWrappedText(const string& txt, unsigned maxWidth) const
350{
351 std::vector<string_view> wrappedLines;
352 for (auto line : StringOp::split_view(txt, '\n')) {
353 do {
354 auto p = splitAtChar(string(line), maxWidth);
355 wrappedLines.push_back(line.substr(0, p));
356 line = line.substr(p);
357 } while (!line.empty());
358 }
359 return join(wrappedLines, '\n');
360}
361
362string OSDText::getWordWrappedText(const string& txt, unsigned maxWidth) const
363{
364 std::vector<string_view> wrappedLines;
365 for (auto line : StringOp::split_view(txt, '\n')) {
366 do {
367 auto p = splitAtWord(string(line), maxWidth);
368 string_view first = line.substr(0, p);
369 StringOp::trimRight(first, ' '); // remove trailing spaces
370 wrappedLines.push_back(first);
371 line = line.substr(p);
372 StringOp::trimLeft(line, ' '); // remove leading spaces
373 } while (!line.empty());
374 }
375 return join(wrappedLines, '\n');
376}
377
378} // 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:28
friend struct SplitAtChar
Definition OSDText.hh:63
void getProperty(std::string_view name, TclObject &result) const override
Definition OSDText.cc:97
void setProperty(Interpreter &interp, std::string_view name, const TclObject &value) override
Definition OSDText.cc:34
std::string_view getType() const override
Definition OSDText.cc:130
int getScaleFactor(const OutputSurface &output) const
Definition OSDWidget.cc:284
void invalidateChildren() const
Definition OSDWidget.cc:253
virtual gl::vec2 getSize(const OutputSurface &output) const =0
OSDWidget * getParent()
Definition OSDWidget.hh:41
void invalidateRecursive()
Definition OSDWidget.cc:247
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:153
gl::ivec2 getSize(zstring_view text) const
Return the size in pixels of the text if it would be rendered.
Definition TTFFont.cc:260
float getFloat(Interpreter &interp) const
Definition TclObject.cc:107
int getInt(Interpreter &interp) const
Definition TclObject.cc:69
zstring_view getString() const
Definition TclObject.cc:141
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:33
void trimLeft(string &str, const char *chars)
Definition StringOp.cc:62
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:181
vecN< 2, float > vec2
Definition gl_vec.hh:178
constexpr vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
Definition gl_vec.hh:302
constexpr vecN< N, T > max(const vecN< N, T > &x, const vecN< N, T > &y)
Definition gl_vec.hh:320
constexpr mat4 scale(const vec3 &xyz)
bool isRegularFile(const Stat &st)
This file implemented 3 utility functions:
Definition Autofire.cc:11
const FileContext & systemFileContext()
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:338
size_t operator()(const string &line, unsigned maxWidth) const
Definition OSDText.cc:339
const OSDText & osdText
Definition OSDText.cc:342
#define UNREACHABLE