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