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