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