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