openMSX
OSDText.cc
Go to the documentation of this file.
1 #include "OSDText.hh"
2 #include "TTFFont.hh"
3 #include "SDLImage.hh"
4 #include "Display.hh"
5 #include "CommandException.hh"
6 #include "FileContext.hh"
7 #include "FileOperations.hh"
8 #include "TclObject.hh"
9 #include "StringOp.hh"
10 #include "join.hh"
11 #include "stl.hh"
12 #include "unreachable.hh"
13 #include "utf8_core.hh"
14 #include "components.hh"
15 #include <cassert>
16 #include <cmath>
17 #include <memory>
18 #if COMPONENT_GL
19 #include "GLImage.hh"
20 #endif
21 
22 using std::string;
23 using std::vector;
24 using namespace gl;
25 
26 namespace openmsx {
27 
28 OSDText::OSDText(Display& display_, const TclObject& name_)
29  : OSDImageBasedWidget(display_, name_)
30  , fontfile("skins/Vera.ttf.gz")
31  , size(12)
32  , wrapMode(NONE), wrapw(0.0), wraprelw(1.0)
33 {
34 }
35 
36 vector<string_view> 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  append(result, vals);
44  return result;
45 }
46 
48  Interpreter& interp, string_view propName, const TclObject& value)
49 {
50  if (propName == "-text") {
51  string_view val = value.getString();
52  if (text != val) {
53  text = val.str();
54  // note: don't invalidate font (don't reopen font file)
57  }
58  } else if (propName == "-font") {
59  string val = value.getString().str();
60  if (fontfile != val) {
61  string file = systemFileContext().resolve(val);
62  if (!FileOperations::isRegularFile(file)) {
63  throw CommandException("Not a valid font file: ", val);
64  }
65  fontfile = val;
67  }
68  } else if (propName == "-size") {
69  int size2 = value.getInt(interp);
70  if (size != size2) {
71  size = size2;
73  }
74  } else if (propName == "-wrap") {
75  string_view val = value.getString();
76  WrapMode wrapMode2;
77  if (val == "none") {
78  wrapMode2 = NONE;
79  } else if (val == "word") {
80  wrapMode2 = WORD;
81  } else if (val == "char") {
82  wrapMode2 = CHAR;
83  } else {
84  throw CommandException("Not a valid value for -wrap, "
85  "expected one of 'none word char', but got '",
86  val, "'.");
87  }
88  if (wrapMode != wrapMode2) {
89  wrapMode = wrapMode2;
91  }
92  } else if (propName == "-wrapw") {
93  float wrapw2 = value.getDouble(interp);
94  if (wrapw != wrapw2) {
95  wrapw = wrapw2;
97  }
98  } else if (propName == "-wraprelw") {
99  float wraprelw2 = value.getDouble(interp);
100  if (wraprelw != wraprelw2) {
101  wraprelw = wraprelw2;
103  }
104  } else if (propName == "-query-size") {
105  throw CommandException("-query-size property is readonly");
106  } else {
107  OSDImageBasedWidget::setProperty(interp, propName, value);
108  }
109 }
110 
111 void OSDText::getProperty(string_view propName, TclObject& result) const
112 {
113  if (propName == "-text") {
114  result = text;
115  } else if (propName == "-font") {
116  result = fontfile;
117  } else if (propName == "-size") {
118  result = size;
119  } else if (propName == "-wrap") {
120  string wrapString;
121  switch (wrapMode) {
122  case NONE: wrapString = "none"; break;
123  case WORD: wrapString = "word"; break;
124  case CHAR: wrapString = "char"; break;
125  default: UNREACHABLE;
126  }
127  result = wrapString;
128  } else if (propName == "-wrapw") {
129  result = wrapw;
130  } else if (propName == "-wraprelw") {
131  result = wraprelw;
132  } else if (propName == "-query-size") {
133  vec2 renderedSize = getRenderedSize();
134  result.addListElement(renderedSize[0], 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 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 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 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
std::vector< string_view > getProperties() const override
Definition: OSDText.cc:36
bool isRegularFile(const Stat &st)
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:306
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:148
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:118
string_view getString() const
Definition: TclObject.cc:102
vector< string_view > split(string_view str, char chars)
Definition: StringOp.cc:197
A frame buffer where pixels can be written to.
void setProperty(Interpreter &interp, string_view name, const TclObject &value) override
Definition: OSDText.cc:47
vecN< N, T > max(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:287
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
std::string resolve(string_view filename) const
Definition: FileContext.cc:76
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:24
void trimRight(string &str, const char *chars)
Definition: StringOp.cc:87
void append(Result &)
Definition: stl.hh:354
int getScaleFactor(const OutputSurface &output) const
Definition: OSDWidget.cc:363
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 invalidateChildren()
Definition: OSDWidget.cc:312
const OSDText & osdText
Definition: OSDText.cc:361
double getDouble(Interpreter &interp) const
Definition: TclObject.cc:92
OSDWidget * getParent()
Definition: OSDWidget.hh:27
This class implements a (close approximation) of the std::string_view class.
Definition: string_view.hh:16
int getInt(Interpreter &interp) const
Definition: TclObject.cc:72
void addListElement(T t)
Definition: TclObject.hh:121
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:111
string_view substr(size_type pos, size_type n=npos) const
Definition: string_view.cc:32
Display & getDisplay() const
Definition: OSDWidget.hh:51
friend struct SplitAtChar
Definition: OSDText.hh:50
std::vector< string_view > getProperties() const override
OutputSurface * getOutputSurface()
Definition: Display.cc:118
constexpr auto size(const C &c) -> decltype(c.size())
Definition: span.hh:62
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
detail::Joiner< Collection, Separator > join(Collection &&col, Separator &&sep)
Definition: join.hh:60
Definition: gl_mat.hh:24
string_view getType() const override
Definition: OSDText.cc:147
#define UNREACHABLE
Definition: unreachable.hh:38