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