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