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