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