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