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