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