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