The Battle for Wesnoth  1.17.10+dev
text.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2022
3  by Mark de Wever <koraq@xs4all.nl>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #define GETTEXT_DOMAIN "wesnoth-lib"
17 
18 #include "font/text.hpp"
19 
20 #include "font/font_config.hpp"
21 
22 #include "font/pango/escape.hpp"
23 #include "font/pango/font.hpp"
24 #include "font/pango/hyperlink.hpp"
26 
27 #include "gettext.hpp"
28 #include "gui/widgets/helper.hpp"
29 #include "gui/core/log.hpp"
30 #include "sdl/point.hpp"
31 #include "sdl/utils.hpp"
34 #include "preferences/general.hpp"
35 #include "video.hpp"
36 
37 #include <boost/algorithm/string/replace.hpp>
38 #include <boost/functional/hash_fwd.hpp>
39 
40 #include <cassert>
41 #include <cstring>
42 #include <stdexcept>
43 
44 static lg::log_domain log_font("font");
45 #define DBG_FT LOG_STREAM(debug, log_font)
46 
47 namespace font
48 {
49 
50 namespace
51 {
52 /**
53  * The text texture cache.
54  *
55  * Each time a specific bit of text is rendered, a corresponding texture is created and
56  * added to the cache. We don't store the surface since there isn't really any use for
57  * it. If we need texture size that can be easily queried.
58  *
59  * @todo Figure out how this can be optimized with a texture atlas. It should be possible
60  * to store smaller bits of text in the atlas and construct new textures from hem.
61  */
62 std::map<std::size_t, texture> rendered_cache{};
63 } // anon namespace
64 
66 {
67  rendered_cache.clear();
68 }
69 
71  : context_(pango_font_map_create_context(pango_cairo_font_map_get_default()), g_object_unref)
72  , layout_(pango_layout_new(context_.get()), g_object_unref)
73  , rect_()
74  , text_()
75  , markedup_text_(false)
76  , link_aware_(false)
77  , link_color_()
78  , font_class_(font::FONT_SANS_SERIF)
79  , font_size_(14)
80  , font_style_(STYLE_NORMAL)
81  , foreground_color_() // solid white
82  , add_outline_(false)
83  , maximum_width_(-1)
84  , characters_per_line_(0)
85  , maximum_height_(-1)
86  , ellipse_mode_(PANGO_ELLIPSIZE_END)
87  , alignment_(PANGO_ALIGN_LEFT)
88  , maximum_length_(std::string::npos)
89  , calculation_dirty_(true)
90  , length_(0)
91  , pixel_scale_(1)
92  , surface_buffer_()
93 {
94  // With 72 dpi the sizes are the same as with SDL_TTF so hardcoded.
95  pango_cairo_context_set_resolution(context_.get(), 72.0);
96 
97  pango_layout_set_ellipsize(layout_.get(), ellipse_mode_);
98  pango_layout_set_alignment(layout_.get(), alignment_);
99  pango_layout_set_wrap(layout_.get(), PANGO_WRAP_WORD_CHAR);
100  pango_layout_set_line_spacing(layout_.get(), 1.3f);
101 
102  cairo_font_options_t *fo = cairo_font_options_create();
103  cairo_font_options_set_hint_style(fo, CAIRO_HINT_STYLE_FULL);
104  cairo_font_options_set_hint_metrics(fo, CAIRO_HINT_METRICS_ON);
105  cairo_font_options_set_antialias(fo, CAIRO_ANTIALIAS_DEFAULT);
106 
107  pango_cairo_context_set_font_options(context_.get(), fo);
108  cairo_font_options_destroy(fo);
109 }
110 
111 texture pango_text::render_texture(const SDL_Rect& viewport)
112 {
113  return with_draw_scale(texture(render_surface(viewport)));
114 }
115 
117 {
118  // Update our settings then hash them.
119  update_pixel_scale(); // TODO: this should be in recalculate()
120  recalculate();
121  const std::size_t hash = std::hash<pango_text>{}(*this);
122  // If we already have the appropriate texture in-cache, use it.
123  if(const auto iter = rendered_cache.find(hash); iter != rendered_cache.end()) {
124  return with_draw_scale(iter->second);
125  }
126 
127  if(surface text_surf = create_surface(); text_surf) {
128  const auto& [new_iter, added] = rendered_cache.try_emplace(hash, std::move(text_surf));
129  return with_draw_scale(new_iter->second);
130  }
131 
132  // Render output was null for some reason. Don't cache.
133  return {};
134 }
135 
136 surface pango_text::render_surface(const SDL_Rect& viewport)
137 {
138  update_pixel_scale(); // TODO: this should be in recalculate()
139  recalculate();
140  return create_surface(viewport);
141 }
142 
144 {
145  update_pixel_scale(); // TODO: this should be in recalculate()
146  recalculate();
147  return create_surface();
148 }
149 
151 {
152  texture res(t);
154  return res;
155 }
156 
158 {
159  return (i + pixel_scale_ - 1) / pixel_scale_;
160 }
161 
163 {
164  // Round up, rather than truncating.
165  return {to_draw_scale(p.x), to_draw_scale(p.y)};
166 }
167 
169 {
170  update_pixel_scale(); // TODO: this should be in recalculate()
171  this->recalculate();
172 
173  return to_draw_scale({rect_.width, rect_.height});
174 }
175 
177 {
178  this->recalculate();
179 
180  return (pango_layout_is_ellipsized(layout_.get()) != 0);
181 }
182 
183 unsigned pango_text::insert_text(const unsigned offset, const std::string& text)
184 {
185  if (text.empty() || length_ == maximum_length_) {
186  return 0;
187  }
188 
189  // do we really need that assert? utf8::insert will just append in this case, which seems fine
190  assert(offset <= length_);
191 
192  unsigned len = utf8::size(text);
193  if (length_ + len > maximum_length_) {
194  len = maximum_length_ - length_;
195  }
196  const std::string insert = text.substr(0, utf8::index(text, len));
197  std::string tmp = text_;
198  this->set_text(utf8::insert(tmp, offset, insert), false);
199  // report back how many characters were actually inserted (e.g. to move the cursor selection)
200  return len;
201 }
202 
203 point pango_text::get_cursor_position(const unsigned column, const unsigned line) const
204 {
205  this->recalculate();
206 
207  // First we need to determine the byte offset, if more routines need it it
208  // would be a good idea to make it a separate function.
209  std::unique_ptr<PangoLayoutIter, std::function<void(PangoLayoutIter*)>> itor(
210  pango_layout_get_iter(layout_.get()), pango_layout_iter_free);
211 
212  // Go the wanted line.
213  if(line != 0) {
214  if(pango_layout_get_line_count(layout_.get()) >= static_cast<int>(line)) {
215  return point(0, 0);
216  }
217 
218  for(std::size_t i = 0; i < line; ++i) {
219  pango_layout_iter_next_line(itor.get());
220  }
221  }
222 
223  // Go the wanted column.
224  for(std::size_t i = 0; i < column; ++i) {
225  if(!pango_layout_iter_next_char(itor.get())) {
226  // It seems that the documentation is wrong and causes and off by
227  // one error... the result should be false if already at the end of
228  // the data when started.
229  if(i + 1 == column) {
230  break;
231  }
232  // We are beyond data.
233  return point(0, 0);
234  }
235  }
236 
237  // Get the byte offset
238  const int offset = pango_layout_iter_get_index(itor.get());
239 
240  // Convert the byte offset in a position.
241  PangoRectangle rect;
242  pango_layout_get_cursor_pos(layout_.get(), offset, &rect, nullptr);
243 
244  return to_draw_scale({PANGO_PIXELS(rect.x), PANGO_PIXELS(rect.y)});
245 }
246 
248 {
249  return maximum_length_;
250 }
251 
252 std::string pango_text::get_token(const point & position, const char * delim) const
253 {
254  this->recalculate();
255 
256  // Get the index of the character.
257  int index, trailing;
258  if (!pango_layout_xy_to_index(layout_.get(), position.x * PANGO_SCALE,
259  position.y * PANGO_SCALE, &index, &trailing)) {
260  return "";
261  }
262 
263  std::string txt = pango_layout_get_text(layout_.get());
264 
265  std::string d(delim);
266 
267  if (index < 0 || (static_cast<std::size_t>(index) >= txt.size()) || d.find(txt.at(index)) != std::string::npos) {
268  return ""; // if the index is out of bounds, or the index character is a delimiter, return nothing
269  }
270 
271  std::size_t l = index;
272  while (l > 0 && (d.find(txt.at(l-1)) == std::string::npos)) {
273  --l;
274  }
275 
276  std::size_t r = index + 1;
277  while (r < txt.size() && (d.find(txt.at(r)) == std::string::npos)) {
278  ++r;
279  }
280 
281  return txt.substr(l,r-l);
282 }
283 
284 std::string pango_text::get_link(const point & position) const
285 {
286  if (!link_aware_) {
287  return "";
288  }
289 
290  std::string tok = this->get_token(position, " \n\r\t");
291 
292  if (looks_like_url(tok)) {
293  return tok;
294  } else {
295  return "";
296  }
297 }
298 
300 {
301  this->recalculate();
302 
303  // Get the index of the character.
304  int index, trailing;
305  pango_layout_xy_to_index(layout_.get(), position.x * PANGO_SCALE,
306  position.y * PANGO_SCALE, &index, &trailing);
307 
308  // Extract the line and the offset in pixels in that line.
309  int line, offset;
310  pango_layout_index_to_line_x(layout_.get(), index, trailing, &line, &offset);
311  offset = PANGO_PIXELS(offset);
312 
313  // Now convert this offset to a column, this way is a bit hacky but haven't
314  // found a better solution yet.
315 
316  /**
317  * @todo There's still a bug left. When you select a text which is in the
318  * ellipses on the right side the text gets reformatted with ellipses on
319  * the left and the selected character is not the one under the cursor.
320  * Other widget toolkits don't show ellipses and have no indication more
321  * text is available. Haven't found what the best thing to do would be.
322  * Until that time leave it as is.
323  */
324  for(std::size_t i = 0; ; ++i) {
325  const int pos = this->get_cursor_position(i, line).x;
326 
327  if(pos == offset) {
328  return point(i, line);
329  }
330  }
331 }
332 
333 bool pango_text::set_text(const std::string& text, const bool markedup)
334 {
335  if(markedup != markedup_text_ || text != text_) {
336  if(layout_ == nullptr) {
337  layout_.reset(pango_layout_new(context_.get()));
338  }
339 
340  const std::u32string wide = unicode_cast<std::u32string>(text);
341  const std::string narrow = unicode_cast<std::string>(wide);
342  if(text != narrow) {
343  ERR_GUI_L << "pango_text::" << __func__
344  << " text '" << text
345  << "' contains invalid utf-8, trimmed the invalid parts.";
346  }
347  if(markedup) {
348  if(!this->set_markup(narrow, *layout_)) {
349  return false;
350  }
351  } else {
352  /*
353  * pango_layout_set_text after pango_layout_set_markup might
354  * leave the layout in an undefined state regarding markup so
355  * clear it unconditionally.
356  */
357  pango_layout_set_attributes(layout_.get(), nullptr);
358  pango_layout_set_text(layout_.get(), narrow.c_str(), narrow.size());
359  }
360  text_ = narrow;
361  length_ = wide.size();
362  markedup_text_ = markedup;
363  calculation_dirty_ = true;
364  }
365 
366  return true;
367 }
368 
370 {
371  if(fclass != font_class_) {
372  font_class_ = fclass;
373  calculation_dirty_ = true;
374  }
375 
376  return *this;
377 }
378 
380 {
381  font_size = preferences::font_scaled(font_size) * pixel_scale_;
382 
383  if(font_size != font_size_) {
384  font_size_ = font_size;
385  calculation_dirty_ = true;
386  }
387 
388  return *this;
389 }
390 
392 {
393  if(font_style != font_style_) {
394  font_style_ = font_style;
395  calculation_dirty_ = true;
396  }
397 
398  return *this;
399 }
400 
402 {
403  if(color != foreground_color_) {
404  foreground_color_ = color;
405  }
406 
407  return *this;
408 }
409 
411 {
412  width *= pixel_scale_;
413 
414  if(width <= 0) {
415  width = -1;
416  }
417 
418  if(width != maximum_width_) {
419  maximum_width_ = width;
420  calculation_dirty_ = true;
421  }
422 
423  return *this;
424 }
425 
426 pango_text& pango_text::set_characters_per_line(const unsigned characters_per_line)
427 {
428  if(characters_per_line != characters_per_line_) {
429  characters_per_line_ = characters_per_line;
430 
431  calculation_dirty_ = true;
432  }
433 
434  return *this;
435 }
436 
437 pango_text& pango_text::set_maximum_height(int height, bool multiline)
438 {
439  height *= pixel_scale_;
440 
441  if(height <= 0) {
442  height = -1;
443  multiline = false;
444  }
445 
446  if(height != maximum_height_) {
447  // assert(context_);
448 
449  // The maximum height is handled in this class' calculate_size() method.
450  //
451  // Although we also pass it to PangoLayout if multiline is true, the documentation of pango_layout_set_height
452  // makes me wonder whether we should avoid that function completely. For example, "at least one line is included
453  // in each paragraph regardless" and "may be changed in future, file a bug if you rely on the current behavior".
454  pango_layout_set_height(layout_.get(), !multiline ? -1 : height * PANGO_SCALE);
455  maximum_height_ = height;
456  calculation_dirty_ = true;
457  }
458 
459  return *this;
460 }
461 
462 pango_text& pango_text::set_ellipse_mode(const PangoEllipsizeMode ellipse_mode)
463 {
464  if(ellipse_mode != ellipse_mode_) {
465  // assert(context_);
466 
467  pango_layout_set_ellipsize(layout_.get(), ellipse_mode);
468  ellipse_mode_ = ellipse_mode;
469  calculation_dirty_ = true;
470  }
471 
472  // According to the docs of pango_layout_set_height, the behavior is undefined if a height other than -1 is combined
473  // with PANGO_ELLIPSIZE_NONE. Wesnoth's code currently always calls set_ellipse_mode after set_maximum_height, so do
474  // the cleanup here. The code in calculate_size() will still apply the maximum height after Pango's calculations.
475  if(ellipse_mode_ == PANGO_ELLIPSIZE_NONE) {
476  pango_layout_set_height(layout_.get(), -1);
477  }
478 
479  return *this;
480 }
481 
482 pango_text &pango_text::set_alignment(const PangoAlignment alignment)
483 {
484  if (alignment != alignment_) {
485  pango_layout_set_alignment(layout_.get(), alignment);
486  alignment_ = alignment;
487  }
488 
489  return *this;
490 }
491 
492 pango_text& pango_text::set_maximum_length(const std::size_t maximum_length)
493 {
494  if(maximum_length != maximum_length_) {
495  maximum_length_ = maximum_length;
496  if(length_ > maximum_length_) {
497  std::string tmp = text_;
498  this->set_text(utf8::truncate(tmp, maximum_length_), false);
499  }
500  }
501 
502  return *this;
503 }
504 
506 {
507  if (link_aware_ != b) {
508  calculation_dirty_ = true;
509  link_aware_ = b;
510  }
511  return *this;
512 }
513 
515 {
516  if(color != link_color_) {
517  link_color_ = color;
518  calculation_dirty_ = true;
519  }
520 
521  return *this;
522 }
523 
525 {
526  if(do_add != add_outline_) {
527  add_outline_ = do_add;
528  //calculation_dirty_ = true;
529  }
530 
531  return *this;
532 }
533 
535 {
537 
538  PangoFont* f = pango_font_map_load_font(
539  pango_cairo_font_map_get_default(),
540  context_.get(),
541  font.get());
542 
543  PangoFontMetrics* m = pango_font_get_metrics(f, nullptr);
544 
545  auto ascent = pango_font_metrics_get_ascent(m);
546  auto descent = pango_font_metrics_get_descent(m);
547 
548  pango_font_metrics_unref(m);
549  g_object_unref(f);
550 
551  return ceil(pango_units_to_double(ascent + descent) / pixel_scale_);
552 }
553 
555 {
556  const int ps = video::get_pixel_scale();
557  if (ps == pixel_scale_) {
558  return;
559  }
560 
562 
563  if (maximum_width_ != -1) {
565  }
566 
567  if (maximum_height_ != -1) {
569  }
570 
571  calculation_dirty_ = true;
572  pixel_scale_ = ps;
573 }
574 
576 {
577  // TODO: clean up this "const everything then mutable everything" mess.
578  // update_pixel_scale() should go in here. But it can't. Because things
579  // are declared const which are not const.
580 
581  if(calculation_dirty_) {
582  assert(layout_ != nullptr);
583 
584  calculation_dirty_ = false;
586  }
587 }
588 
589 PangoRectangle pango_text::calculate_size(PangoLayout& layout) const
590 {
591  PangoRectangle size;
592 
594  pango_layout_set_font_description(&layout, font.get());
595 
596  if(font_style_ & pango_text::STYLE_UNDERLINE) {
597  PangoAttrList *attribute_list = pango_attr_list_new();
598  pango_attr_list_insert(attribute_list
599  , pango_attr_underline_new(PANGO_UNDERLINE_SINGLE));
600 
601  pango_layout_set_attributes(&layout, attribute_list);
602  pango_attr_list_unref(attribute_list);
603  }
604 
605  int maximum_width = 0;
606  if(characters_per_line_ != 0) {
607  PangoFont* f = pango_font_map_load_font(
608  pango_cairo_font_map_get_default(),
609  context_.get(),
610  font.get());
611 
612  PangoFontMetrics* m = pango_font_get_metrics(f, nullptr);
613 
614  int w = pango_font_metrics_get_approximate_char_width(m);
616 
617  maximum_width = ceil(pango_units_to_double(w));
618 
619  pango_font_metrics_unref(m);
620  g_object_unref(f);
621  } else {
622  maximum_width = maximum_width_;
623  }
624 
625  if(maximum_width_ != -1) {
626  maximum_width = std::min(maximum_width, maximum_width_);
627  }
628 
629  pango_layout_set_width(&layout, maximum_width == -1
630  ? -1
631  : maximum_width * PANGO_SCALE);
632  pango_layout_get_pixel_extents(&layout, nullptr, &size);
633 
634  DBG_GUI_L << "pango_text::" << __func__
635  << " text '" << gui2::debug_truncate(text_)
636  << "' maximum_width " << maximum_width
637  << " width " << size.x + size.width
638  << ".";
639 
640  DBG_GUI_L << "pango_text::" << __func__
641  << " text '" << gui2::debug_truncate(text_)
642  << "' font_size " << font_size_
643  << " markedup_text " << markedup_text_
644  << " font_style " << std::hex << font_style_ << std::dec
645  << " maximum_width " << maximum_width
646  << " maximum_height " << maximum_height_
647  << " result " << size
648  << ".";
649 
650  if(maximum_width != -1 && size.x + size.width > maximum_width) {
651  DBG_GUI_L << "pango_text::" << __func__
652  << " text '" << gui2::debug_truncate(text_)
653  << " ' width " << size.x + size.width
654  << " greater as the wanted maximum of " << maximum_width
655  << ".";
656  }
657 
658  // The maximum height is handled here instead of using the library - see the comments in set_maximum_height()
659  if(maximum_height_ != -1 && size.y + size.height > maximum_height_) {
660  DBG_GUI_L << "pango_text::" << __func__
661  << " text '" << gui2::debug_truncate(text_)
662  << " ' height " << size.y + size.height
663  << " greater as the wanted maximum of " << maximum_height_
664  << ".";
665  size.height = maximum_height_ - std::max(0, size.y);
666  }
667 
668  return size;
669 }
670 
671 /***
672  * Inverse table
673  *
674  * Holds a high-precision inverse for each number i, that is, a number x such that x * i / 256 is close to 255.
675  */
677 {
678  unsigned values[256];
679 
681  {
682  values[0] = 0;
683  for (int i = 1; i < 256; ++i) {
684  values[i] = (255 * 256) / i;
685  }
686  }
687 
688  unsigned operator[](uint8_t i) const { return values[i]; }
689 };
690 
692 
693 /***
694  * Helper function for un-premultiplying alpha
695  * Div should be the high-precision inverse for the alpha value.
696  */
697 static void unpremultiply(uint8_t & value, const unsigned div) {
698  unsigned temp = (value * div) / 256u;
699  // Note: It's always the case that alpha * div < 256 if div is the inverse
700  // for alpha, so if cairo is computing premultiplied alpha by rounding down,
701  // this min is not necessary. However, if cairo generates illegal output,
702  // the min may be selected.
703  // It's probably not worth removing the min, since branch prediction will
704  // make it essentially free if one of the branches is never actually
705  // selected.
706  value = std::min(255u, temp);
707 }
708 
709 /**
710  * Converts from cairo-format ARGB32 premultiplied alpha to plain alpha.
711  * @param c a uint32 representing the color
712  */
713 static void from_cairo_format(uint32_t & c)
714 {
715  uint8_t a = (c >> 24) & 0xff;
716  uint8_t r = (c >> 16) & 0xff;
717  uint8_t g = (c >> 8) & 0xff;
718  uint8_t b = c & 0xff;
719 
720  const unsigned div = inverse_table_[a];
721  unpremultiply(r, div);
722  unpremultiply(g, div);
723  unpremultiply(b, div);
724 
725  c = (static_cast<uint32_t>(a) << 24) | (static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | static_cast<uint32_t>(b);
726 }
727 
728 void pango_text::render(PangoLayout& layout, const SDL_Rect& viewport, const unsigned stride)
729 {
730  cairo_format_t format = CAIRO_FORMAT_ARGB32;
731 
732  uint8_t* buffer = &surface_buffer_[0];
733 
734  std::unique_ptr<cairo_surface_t, std::function<void(cairo_surface_t*)>> cairo_surface(
735  cairo_image_surface_create_for_data(buffer, format, viewport.w, viewport.h, stride), cairo_surface_destroy);
736  std::unique_ptr<cairo_t, std::function<void(cairo_t*)>> cr(cairo_create(cairo_surface.get()), cairo_destroy);
737 
738  if(cairo_status(cr.get()) == CAIRO_STATUS_INVALID_SIZE) {
739  throw std::length_error("Text is too long to render");
740  }
741 
742  // The top-left of the text, which can be outside the area to be rendered
743  cairo_move_to(cr.get(), -viewport.x, -viewport.y);
744 
745  //
746  // TODO: the outline may be slightly cut off around certain text if it renders too
747  // close to the surface's edge. That causes the outline to extend just slightly
748  // outside the surface's borders. I'm not sure how best to deal with this. Obviously,
749  // we want to increase the surface size, but we also don't want to invalidate all
750  // the placement and size calculations. Thankfully, it's not very noticeable.
751  //
752  // -- vultraz, 2018-03-07
753  //
754  if(add_outline_) {
755  // Add a path to the cairo context tracing the current text.
756  pango_cairo_layout_path(cr.get(), &layout);
757 
758  // Set color for background outline (black).
759  cairo_set_source_rgba(cr.get(), 0.0, 0.0, 0.0, 1.0);
760 
761  cairo_set_line_join(cr.get(), CAIRO_LINE_JOIN_ROUND);
762  cairo_set_line_width(cr.get(), 3.0); // Adjust as necessary
763 
764  // Stroke path to draw outline.
765  cairo_stroke(cr.get());
766  }
767 
768  // Set main text color.
769  cairo_set_source_rgba(cr.get(),
770  foreground_color_.r / 255.0,
771  foreground_color_.g / 255.0,
772  foreground_color_.b / 255.0,
773  foreground_color_.a / 255.0
774  );
775 
776  pango_cairo_show_layout(cr.get(), &layout);
777 }
778 
780 {
781  return create_surface({0, 0, rect_.x + rect_.width, rect_.y + rect_.height});
782 }
783 
784 surface pango_text::create_surface(const SDL_Rect& viewport)
785 {
786  assert(layout_.get());
787 
788  cairo_format_t format = CAIRO_FORMAT_ARGB32;
789  const int stride = cairo_format_stride_for_width(format, viewport.w);
790 
791  // The width and stride can be zero if the text is empty or the stride can be negative to indicate an error from
792  // Cairo. Width isn't tested here because it's implied by stride.
793  if(stride <= 0 || viewport.h <= 0) {
794  surface_buffer_.clear();
795  return nullptr;
796  }
797 
798  DBG_FT << "creating new text surface";
799 
800  // Check to prevent arithmetic overflow when calculating (stride * height).
801  // The size of the viewport should already provide a far lower limit on the
802  // maximum size, but this is left in as a sanity check.
803  if(viewport.h > std::numeric_limits<int>::max() / stride) {
804  throw std::length_error("Text is too long to render");
805  }
806 
807  // Resize buffer appropriately and set all pixel values to 0.
808  surface_buffer_.assign(viewport.h * stride, 0);
809 
810  // Try rendering the whole text in one go. If this throws a length_error
811  // then leave it to the caller to handle; one reason it may throw is that
812  // cairo surfaces are limited to approximately 2**15 pixels in height.
813  render(*layout_, viewport, stride);
814 
815  // The cairo surface is in CAIRO_FORMAT_ARGB32 which uses
816  // pre-multiplied alpha. SDL doesn't use that so the pixels need to be
817  // decoded again.
818  for(int y = 0; y < viewport.h; ++y) {
819  uint32_t* pixels = reinterpret_cast<uint32_t*>(&surface_buffer_[y * stride]);
820  for(int x = 0; x < viewport.w; ++x) {
821  from_cairo_format(pixels[x]);
822  }
823  }
824 
825  return SDL_CreateRGBSurfaceWithFormatFrom(
826  &surface_buffer_[0], viewport.w, viewport.h, 32, stride, SDL_PIXELFORMAT_ARGB8888);
827 }
828 
829 bool pango_text::set_markup(std::string_view text, PangoLayout& layout)
830 {
831  char* raw_text;
832  std::string semi_escaped;
833  bool valid = validate_markup(text, &raw_text, semi_escaped);
834  if(semi_escaped != "") {
835  text = semi_escaped;
836  }
837 
838  if(valid) {
839  if(link_aware_) {
840  std::string formatted_text = format_links(text);
841  pango_layout_set_markup(&layout, formatted_text.c_str(), formatted_text.size());
842  } else {
843  pango_layout_set_markup(&layout, text.data(), text.size());
844  }
845  } else {
846  ERR_GUI_L << "pango_text::" << __func__
847  << " text '" << text
848  << "' has broken markup, set to normal text.";
849  set_text(_("The text contains invalid Pango markup: ") + std::string(text), false);
850  }
851 
852  return valid;
853 }
854 
855 /**
856  * Replaces all instances of URLs in a given string with formatted links
857  * and returns the result.
858  */
859 std::string pango_text::format_links(std::string_view text) const
860 {
861  static const std::string delim = " \n\r\t";
862  std::ostringstream result;
863 
864  std::size_t tok_start = 0;
865  for(std::size_t pos = 0; pos < text.length(); ++pos) {
866  if(delim.find(text[pos]) == std::string::npos) {
867  continue;
868  }
869 
870  if(const auto tok_length = pos - tok_start) {
871  // Token starts from after the last delimiter up to (but not including) this delimiter
872  auto token = text.substr(tok_start, tok_length);
873  if(looks_like_url(token)) {
874  result << format_as_link(std::string{token}, link_color_);
875  } else {
876  result << token;
877  }
878  }
879 
880  result << text[pos];
881  tok_start = pos + 1;
882  }
883 
884  // Deal with the remainder token
885  if(tok_start < text.length()) {
886  auto token = text.substr(tok_start);
887  if(looks_like_url(token)) {
888  result << format_as_link(std::string{token}, link_color_);
889  } else {
890  result << token;
891  }
892  }
893 
894  return result.str();
895 }
896 
897 bool pango_text::validate_markup(std::string_view text, char** raw_text, std::string& semi_escaped) const
898 {
899  if(pango_parse_markup(text.data(), text.size(),
900  0, nullptr, raw_text, nullptr, nullptr)) {
901  return true;
902  }
903 
904  /*
905  * The markup is invalid. Try to recover.
906  *
907  * The pango engine tested seems to accept stray single quotes »'« and
908  * double quotes »"«. Stray ampersands »&« seem to give troubles.
909  * So only try to recover from broken ampersands, by simply replacing them
910  * with the escaped version.
911  */
912  semi_escaped = semi_escape_text(std::string(text));
913 
914  /*
915  * If at least one ampersand is replaced the semi-escaped string
916  * is longer than the original. If this isn't the case then the
917  * markup wasn't (only) broken by ampersands in the first place.
918  */
919  if(text.size() == semi_escaped.size()
920  || !pango_parse_markup(semi_escaped.c_str(), semi_escaped.size()
921  , 0, nullptr, raw_text, nullptr, nullptr)) {
922 
923  /* Fixing the ampersands didn't work. */
924  return false;
925  }
926 
927  /* Replacement worked, still warn the user about the error. */
928  WRN_GUI_L << "pango_text::" << __func__
929  << " text '" << text
930  << "' has unescaped ampersands '&', escaped them.";
931 
932  return true;
933 }
934 
935 void pango_text::copy_layout_properties(PangoLayout& src, PangoLayout& dst)
936 {
937  pango_layout_set_alignment(&dst, pango_layout_get_alignment(&src));
938  pango_layout_set_height(&dst, pango_layout_get_height(&src));
939  pango_layout_set_ellipsize(&dst, pango_layout_get_ellipsize(&src));
940 }
941 
942 std::vector<std::string> pango_text::get_lines() const
943 {
944  this->recalculate();
945 
946  PangoLayout* const layout = layout_.get();
947  std::vector<std::string> res;
948  int count = pango_layout_get_line_count(layout);
949 
950  if(count < 1) {
951  return res;
952  }
953 
954  using layout_iterator = std::unique_ptr<PangoLayoutIter, std::function<void(PangoLayoutIter*)>>;
955  layout_iterator i{pango_layout_get_iter(layout), pango_layout_iter_free};
956 
957  res.reserve(count);
958 
959  do {
960  PangoLayoutLine* ll = pango_layout_iter_get_line_readonly(i.get());
961  const char* begin = &pango_layout_get_text(layout)[ll->start_index];
962  res.emplace_back(begin, ll->length);
963  } while(pango_layout_iter_next_line(i.get()));
964 
965  return res;
966 }
967 
969 {
970  static pango_text text_renderer;
971  return text_renderer;
972 }
973 
975 {
976  // Reset metrics to defaults
977  return get_text_renderer()
978  .set_family_class(fclass)
979  .set_font_style(style)
980  .set_font_size(size)
982 }
983 
984 } // namespace font
985 
986 namespace std
987 {
988 std::size_t hash<font::pango_text>::operator()(const font::pango_text& t) const
989 {
990  std::size_t hash = 0;
991 
992  boost::hash_combine(hash, t.text_);
993  boost::hash_combine(hash, t.font_class_);
994  boost::hash_combine(hash, t.font_size_);
995  boost::hash_combine(hash, t.font_style_);
996  boost::hash_combine(hash, t.foreground_color_.to_rgba_bytes());
997  boost::hash_combine(hash, t.rect_.width);
998  boost::hash_combine(hash, t.rect_.height);
999  boost::hash_combine(hash, t.maximum_width_);
1000  boost::hash_combine(hash, t.maximum_height_);
1001  boost::hash_combine(hash, t.alignment_);
1002  boost::hash_combine(hash, t.ellipse_mode_);
1003  boost::hash_combine(hash, t.add_outline_);
1004 
1005  return hash;
1006 }
1007 
1008 } // namespace std
Define the common log macros for the gui toolkit.
void recalculate() const
Recalculates the text layout.
Definition: text.cpp:575
surface render_surface()
Equivalent to render(viewport), where the viewport&#39;s top-left is at (0,0) and the area is large enoug...
Definition: text.cpp:143
unsigned font_size_
The font size to draw.
Definition: text.hpp:309
#define DBG_GUI_L
Definition: log.hpp:55
family_class
Font classes for get_font_families().
int get_pixel_scale()
Get the current active pixel scale multiplier.
Definition: video.cpp:471
bool set_text(const std::string &text, const bool markedup)
Sets the text to render.
Definition: text.cpp:333
Collection of helper functions relating to Pango formatting.
static void unpremultiply(uint8_t &value, const unsigned div)
Definition: text.cpp:697
bool set_markup(std::string_view text, PangoLayout &layout)
Sets the markup&#39;ed text.
Definition: text.cpp:829
texture render_and_get_texture()
Returns the cached texture, or creates a new one otherwise.
Definition: text.cpp:116
texture with_draw_scale(const texture &t) const
Adjust a texture&#39;s draw-width and height according to pixel scale.
Definition: text.cpp:150
static void from_cairo_format(uint32_t &c)
Converts from cairo-format ARGB32 premultiplied alpha to plain alpha.
Definition: text.cpp:713
int maximum_height_
The maximum height of the text.
Definition: text.hpp:354
#define a
unsigned characters_per_line_
The number of characters per line.
Definition: text.hpp:346
point get_raw_size() const
The raw internal texture size.
Definition: texture.cpp:112
ucs4_convert_impl::enableif< TD, typename TS::value_type >::type unicode_cast(const TS &source)
texture render_texture(const SDL_Rect &viewport)
Returns the rendered text as a texture.
Definition: text.cpp:111
std::size_t length_
Length of the text.
Definition: text.hpp:375
#define WRN_GUI_L
Definition: log.hpp:57
std::vector< std::string > get_lines() const
Retrieves a list of strings with contents for each rendered line.
Definition: text.cpp:942
pango_text & set_link_aware(bool b)
Definition: text.cpp:505
#define DBG_FT
Definition: text.cpp:45
STL namespace.
std::size_t maximum_length_
The maximum length of the text.
Definition: text.hpp:363
#define d
std::size_t get_maximum_length() const
Get maximum length.
Definition: text.cpp:247
font::family_class font_class_
The font family class used.
Definition: text.hpp:306
pango_text & set_font_style(const FONT_STYLE font_style)
Definition: text.cpp:391
point get_size()
Returns the size of the text, in drawing coordinates.
Definition: text.cpp:168
static std::string _(const char *str)
Definition: gettext.hpp:93
pango_text & get_text_renderer()
Returns a reference to a static pango_text object.
Definition: text.cpp:968
int get_max_height(unsigned size, font::family_class fclass, pango_text::FONT_STYLE style)
Returns the maximum glyph height of a font, in pixels.
Definition: text.cpp:974
pango_text & set_ellipse_mode(const PangoEllipsizeMode ellipse_mode)
Definition: text.cpp:462
pango_text & set_maximum_length(const std::size_t maximum_length)
Definition: text.cpp:492
std::vector< uint8_t > surface_buffer_
Buffer to store the image on.
Definition: text.hpp:402
bool validate_markup(std::string_view text, char **raw_text, std::string &semi_escaped) const
Definition: text.cpp:897
Small helper class to make sure the pango font object is destroyed properly.
Definition: font.hpp:24
Wrapper class to encapsulate creation and management of an SDL_Texture.
Definition: texture.hpp:32
void rect(const SDL_Rect &rect)
Draw a rectangle.
Definition: draw.cpp:141
static void layout()
pango_text & set_alignment(const PangoAlignment alignment)
Definition: text.cpp:482
#define b
#define ERR_GUI_L
Definition: log.hpp:58
surface create_surface()
Renders the text to a surface.
Definition: text.cpp:779
const t_string & get_font_families(family_class fclass)
Returns the currently defined fonts.
point get_cursor_position(const unsigned column, const unsigned line=0) const
Gets the location for the cursor, in drawing coordinates.
Definition: text.cpp:203
std::size_t size(const std::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:87
std::string get_token(const point &position, const char *delimiters=" \\) const
Gets the largest collection of characters, including the token at position, and not including any cha...
Definition: text.cpp:252
std::unique_ptr< PangoLayout, std::function< void(void *)> > layout_
Definition: text.hpp:284
std::string format_links(std::string_view text) const
Replaces all instances of URLs in a given string with formatted links and returns the result...
Definition: text.cpp:859
bool is_truncated() const
Has the text been truncated? This happens if it exceeds max width or height.
Definition: text.cpp:176
int maximum_width_
The maximum width of the text.
Definition: text.hpp:328
std::string semi_escape_text(const std::string &text)
Definition: escape.hpp:52
color_t foreground_color_
The foreground color.
Definition: text.hpp:315
std::string get_link(const point &position) const
Checks if position points to a character in a link in the text, returns it if so, empty string otherw...
Definition: text.cpp:284
int font_scaled(int size)
Definition: general.cpp:503
static const inverse_table inverse_table_
Definition: text.cpp:691
PangoEllipsizeMode ellipse_mode_
The way too long text is shown depends on this mode.
Definition: text.hpp:357
unsigned operator[](uint8_t i) const
Definition: text.cpp:688
pango_text & set_characters_per_line(const unsigned characters_per_line)
Definition: text.cpp:426
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:58
std::string & truncate(std::string &str, const std::size_t size)
Truncates a UTF-8 string to the specified number of characters.
Definition: unicode.cpp:118
bool markedup_text_
Does the text contain pango markup? If different render routines must be used.
Definition: text.hpp:291
PangoRectangle calculate_size(PangoLayout &layout) const
Calculates surface size.
Definition: text.cpp:589
int pixel_scale_
The pixel scale, used to render high-DPI text.
Definition: text.hpp:378
PangoRectangle rect_
Definition: text.hpp:285
void set_draw_size(int w, int h)
Set the intended size of the texture, in draw-space.
Definition: texture.hpp:131
bool calculation_dirty_
The text has two dirty states:
Definition: text.hpp:372
unsigned insert_text(const unsigned offset, const std::string &text)
Inserts UTF-8 text.
Definition: text.cpp:183
pango_text & set_family_class(font::family_class fclass)
Definition: text.cpp:369
bool looks_like_url(std::string_view str)
Definition: hyperlink.hpp:27
std::unique_ptr< PangoContext, std::function< void(void *)> > context_
Definition: text.hpp:283
bool link_aware_
Are hyperlinks in the text marked-up, and will get_link return them.
Definition: text.hpp:294
std::size_t i
Definition: function.cpp:967
void line(int from_x, int from_y, int to_x, int to_y)
Draw a line.
Definition: draw.cpp:171
mock_party p
point get_column_line(const point &position) const
Gets the column of line of the character at the position.
Definition: text.cpp:299
std::string debug_truncate(const std::string &text)
Returns a truncated version of the text.
Definition: helper.cpp:119
pango_text & set_font_size(unsigned font_size)
Definition: text.cpp:379
constexpr uint32_t to_rgba_bytes() const
Returns the stored color as a uint32_t, in RGBA format.
Definition: color.hpp:149
double g
Definition: astarsearch.cpp:65
static lg::log_domain log_font("font")
CURSOR_TYPE get()
Definition: cursor.cpp:216
void update_pixel_scale()
Update pixel scale, if necessary.
Definition: text.cpp:554
FONT_STYLE font_style_
The style of the font, this is an orred mask of the font flags.
Definition: text.hpp:312
int get_max_glyph_height() const
Returns the maximum glyph height of a font, in drawing coordinates.
Definition: text.cpp:534
bool add_outline_
Whether to add an outline effect.
Definition: text.hpp:318
Holds a 2D point.
Definition: point.hpp:24
pango_text & set_link_color(const color_t &color)
Definition: text.cpp:514
void flush_texture_cache()
Flush the rendered text cache.
Definition: text.cpp:65
std::string & insert(std::string &str, const std::size_t pos, const std::string &insert)
Insert a UTF-8 string at the specified position.
Definition: unicode.cpp:100
int w
std::size_t index(const std::string &str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:72
const std::string & text() const
Definition: text.hpp:250
color_t link_color_
The color to render links in.
Definition: text.hpp:303
Text class.
Definition: text.hpp:79
std::string format_as_link(const std::string &link, color_t color)
Definition: hyperlink.hpp:32
pango_text & set_maximum_width(int width)
Definition: text.cpp:410
#define f
double t
Definition: astarsearch.cpp:65
int to_draw_scale(int s) const
Scale the given render-space size to draw-space, rounding up.
Definition: text.cpp:157
void point(int x, int y)
Draw a single point.
Definition: draw.cpp:193
void render(PangoLayout &layout, const SDL_Rect &viewport, const unsigned stride)
Definition: text.cpp:728
pango_text & set_maximum_height(int height, bool multiline)
Definition: text.cpp:437
mock_char c
std::string text_
The text to draw (stored as UTF-8).
Definition: text.hpp:288
pango_text & set_foreground_color(const color_t &color)
Definition: text.cpp:401
PangoAlignment alignment_
The alignment of the text.
Definition: text.hpp:360
pango_text & set_add_outline(bool do_add)
Definition: text.cpp:524
static void copy_layout_properties(PangoLayout &src, PangoLayout &dst)
Definition: text.cpp:935