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