The Battle for Wesnoth  1.19.3+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  this->recalculate();
154 
155  return to_draw_scale({rect_.width, rect_.height});
156 }
157 
159 {
160  this->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)
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  this->set_text(utf8::insert(tmp, offset, insert), false);
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  this->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  this->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 = this->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  this->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 = this->get_cursor_position(i, line).x;
313 
314  if(pos == offset) {
315  return point(i, line);
316  }
317  }
318 }
319 
320 void pango_text::add_attribute_size(const unsigned start_offset, const unsigned end_offset, int size)
321 {
322  attribute_start_offset_ = start_offset;
323  attribute_end_offset_ = end_offset;
324 
326  PangoAttribute *attr = pango_attr_size_new_absolute(PANGO_SCALE * size);
327  attr->start_index = attribute_start_offset_;
328  attr->end_index = attribute_end_offset_;
329 
330  DBG_GUI_D << "attribute: size";
331  DBG_GUI_D << "attribute start: " << start_offset << " end : " << end_offset;
332 
333  // Insert all attributes
334  pango_attr_list_insert(global_attribute_list_, attr);
335  }
336 }
337 
338 void pango_text::add_attribute_weight(const unsigned start_offset, const unsigned end_offset, PangoWeight weight)
339 {
340  attribute_start_offset_ = start_offset;
341  attribute_end_offset_ = end_offset;
342 
344  PangoAttribute *attr = pango_attr_weight_new(weight);
345  attr->start_index = attribute_start_offset_;
346  attr->end_index = attribute_end_offset_;
347 
348  DBG_GUI_D << "attribute: weight";
349  DBG_GUI_D << "attribute start: " << start_offset << " end : " << end_offset;
350 
351  // Insert all attributes
352  pango_attr_list_insert(global_attribute_list_, attr);
353  }
354 }
355 
356 void pango_text::add_attribute_style(const unsigned start_offset, const unsigned end_offset, PangoStyle style)
357 {
358  attribute_start_offset_ = start_offset;
359  attribute_end_offset_ = end_offset;
360 
362 
363  PangoAttribute *attr = pango_attr_style_new(style);
364  attr->start_index = attribute_start_offset_;
365  attr->end_index = attribute_end_offset_;
366 
367  DBG_GUI_D << "attribute: style";
368  DBG_GUI_D << "attribute start: " << attribute_start_offset_ << " end : " << attribute_end_offset_;
369 
370  // Insert all attributes
371  pango_attr_list_insert(global_attribute_list_, attr);
372  }
373 }
374 
375 void pango_text::add_attribute_underline(const unsigned start_offset, const unsigned end_offset, PangoUnderline underline)
376 {
377  attribute_start_offset_ = start_offset;
378  attribute_end_offset_ = end_offset;
379 
381  PangoAttribute *attr = pango_attr_underline_new(underline);
382  attr->start_index = attribute_start_offset_;
383  attr->end_index = attribute_end_offset_;
384 
385  DBG_GUI_D << "attribute: underline";
386  DBG_GUI_D << "attribute start: " << start_offset << " end : " << end_offset;
387 
388  // Insert all attributes
389  pango_attr_list_insert(global_attribute_list_, attr);
390  }
391 }
392 
393 
394 void pango_text::add_attribute_fg_color(const unsigned start_offset, const unsigned end_offset, const color_t& color)
395 {
396  attribute_start_offset_ = start_offset;
397  attribute_end_offset_ = end_offset;
398 
400  int col_r = color.r / 255.0 * 65535.0;
401  int col_g = color.g / 255.0 * 65535.0;
402  int col_b = color.b / 255.0 * 65535.0;
403 
404  PangoAttribute *attr = pango_attr_foreground_new(col_r, col_g, col_b);
405  attr->start_index = start_offset;
406  attr->end_index = end_offset;
407 
408  DBG_GUI_D << "attribute: fg color";
409  DBG_GUI_D << "attribute start: " << attribute_start_offset_ << " end : " << attribute_end_offset_;
410  DBG_GUI_D << "color: " << col_r << "," << col_g << "," << col_b;
411 
412  // Insert all attributes
413  pango_attr_list_insert(global_attribute_list_, attr);
414  }
415 }
416 
417 void pango_text::add_attribute_font_family(const unsigned start_offset, const unsigned end_offset, std::string family)
418 {
419  attribute_start_offset_ = start_offset;
420  attribute_end_offset_ = end_offset;
421 
423  PangoAttribute *attr = pango_attr_family_new(family.c_str());
424  attr->start_index = attribute_start_offset_;
425  attr->end_index = attribute_end_offset_;
426 
427  DBG_GUI_D << "attribute: font family";
428  DBG_GUI_D << "attribute start: " << start_offset << " end : " << end_offset;
429  DBG_GUI_D << "font family: " << family;
430 
431  // Insert all attributes
432  pango_attr_list_insert(global_attribute_list_, attr);
433  }
434 }
435 
436 void pango_text::set_highlight_area(const unsigned start_offset, const unsigned end_offset, const color_t& color) {
437  attribute_start_offset_ = start_offset;
438  attribute_end_offset_ = end_offset;
439  highlight_color_ = color;
440 
441  // Highlight
442  int col_r = highlight_color_.r / 255.0 * 65535.0;
443  int col_g = highlight_color_.g / 255.0 * 65535.0;
444  int col_b = highlight_color_.b / 255.0 * 65535.0;
445 
446  DBG_GUI_D << "highlight start: " << attribute_start_offset_ << "end : " << attribute_end_offset_;
447  DBG_GUI_D << "highlight color: " << col_r << "," << col_g << "," << col_b;
448 
449  PangoAttribute *attr = pango_attr_background_new(col_r, col_g, col_b);
450  attr->start_index = attribute_start_offset_;
451  attr->end_index = attribute_end_offset_;
452 
453  // Insert all attributes
454  pango_attr_list_change(global_attribute_list_, attr);
455 }
456 
458  global_attribute_list_ = pango_attr_list_new();
459  pango_layout_set_attributes(layout_.get(), global_attribute_list_);
460 }
461 
462 bool pango_text::set_text(const std::string& text, const bool markedup)
463 {
464  if(markedup != markedup_text_ || text != text_) {
465  if(layout_ == nullptr) {
466  layout_.reset(pango_layout_new(context_.get()));
467  }
468 
469  const std::u32string wide = unicode_cast<std::u32string>(text);
470  const std::string narrow = unicode_cast<std::string>(wide);
471  if(text != narrow) {
472  ERR_GUI_L << "pango_text::" << __func__
473  << " text '" << text
474  << "' contains invalid utf-8, trimmed the invalid parts.";
475  }
476 
477  pango_layout_set_attributes(layout_.get(), global_attribute_list_);
478  // Clear list. Using pango_attr_list_unref() causes segfault
479  global_attribute_list_ = pango_attr_list_new();
480 
481  if(markedup) {
482  if(!this->set_markup(narrow, *layout_)) {
483  return false;
484  }
485  } else {
487  /*
488  * pango_layout_set_text after pango_layout_set_markup might
489  * leave the layout in an undefined state regarding markup so
490  * clear it unconditionally.
491  */
492  pango_layout_set_attributes(layout_.get(), nullptr);
493  }
494 
495  pango_layout_set_text(layout_.get(), narrow.c_str(), narrow.size());
496  }
497 
498  text_ = narrow;
499  length_ = wide.size();
500  markedup_text_ = markedup;
501  calculation_dirty_ = true;
502  }
503 
504  return true;
505 }
506 
508 {
509  if(fclass != font_class_) {
510  font_class_ = fclass;
511  calculation_dirty_ = true;
512  }
513 
514  return *this;
515 }
516 
518 {
519  font_size = prefs::get().font_scaled(font_size) * pixel_scale_;
520 
521  if(font_size != font_size_) {
522  font_size_ = font_size;
523  calculation_dirty_ = true;
524  }
525 
526  return *this;
527 }
528 
530 {
531  if(font_style != font_style_) {
532  font_style_ = font_style;
533  calculation_dirty_ = true;
534  }
535 
536  return *this;
537 }
538 
540 {
541  if(color != foreground_color_) {
542  foreground_color_ = color;
543  }
544 
545  return *this;
546 }
547 
549 {
550  width *= pixel_scale_;
551 
552  if(width <= 0) {
553  width = -1;
554  }
555 
556  if(width != maximum_width_) {
557  maximum_width_ = width;
558  calculation_dirty_ = true;
559  }
560 
561  return *this;
562 }
563 
564 pango_text& pango_text::set_characters_per_line(const unsigned characters_per_line)
565 {
566  if(characters_per_line != characters_per_line_) {
567  characters_per_line_ = characters_per_line;
568 
569  calculation_dirty_ = true;
570  }
571 
572  return *this;
573 }
574 
575 pango_text& pango_text::set_maximum_height(int height, bool multiline)
576 {
577  height *= pixel_scale_;
578 
579  if(height <= 0) {
580  height = -1;
581  multiline = false;
582  }
583 
584  if(height != maximum_height_) {
585  // assert(context_);
586 
587  // The maximum height is handled in this class' calculate_size() method.
588  //
589  // Although we also pass it to PangoLayout if multiline is true, the documentation of pango_layout_set_height
590  // makes me wonder whether we should avoid that function completely. For example, "at least one line is included
591  // in each paragraph regardless" and "may be changed in future, file a bug if you rely on the current behavior".
592  pango_layout_set_height(layout_.get(), !multiline ? -1 : height * PANGO_SCALE);
593  maximum_height_ = height;
594  calculation_dirty_ = true;
595  }
596 
597  return *this;
598 }
599 
600 pango_text& pango_text::set_ellipse_mode(const PangoEllipsizeMode ellipse_mode)
601 {
602  if(ellipse_mode != ellipse_mode_) {
603  // assert(context_);
604 
605  pango_layout_set_ellipsize(layout_.get(), ellipse_mode);
606  ellipse_mode_ = ellipse_mode;
607  calculation_dirty_ = true;
608  }
609 
610  // According to the docs of pango_layout_set_height, the behavior is undefined if a height other than -1 is combined
611  // with PANGO_ELLIPSIZE_NONE. Wesnoth's code currently always calls set_ellipse_mode after set_maximum_height, so do
612  // the cleanup here. The code in calculate_size() will still apply the maximum height after Pango's calculations.
613  if(ellipse_mode_ == PANGO_ELLIPSIZE_NONE) {
614  pango_layout_set_height(layout_.get(), -1);
615  }
616 
617  return *this;
618 }
619 
620 pango_text &pango_text::set_alignment(const PangoAlignment alignment)
621 {
622  if (alignment != alignment_) {
623  pango_layout_set_alignment(layout_.get(), alignment);
624  alignment_ = alignment;
625  }
626 
627  return *this;
628 }
629 
630 pango_text& pango_text::set_maximum_length(const std::size_t maximum_length)
631 {
632  if(maximum_length != maximum_length_) {
633  maximum_length_ = maximum_length;
634  if(length_ > maximum_length_) {
635  std::string tmp = text_;
636  this->set_text(utf8::truncate(tmp, maximum_length_), false);
637  }
638  }
639 
640  return *this;
641 }
642 
644 {
645  if (link_aware_ != b) {
646  calculation_dirty_ = true;
647  link_aware_ = b;
648  }
649  return *this;
650 }
651 
653 {
654  if(color != link_color_) {
655  link_color_ = color;
656  calculation_dirty_ = true;
657  }
658 
659  return *this;
660 }
661 
663 {
664  if(do_add != add_outline_) {
665  add_outline_ = do_add;
666  //calculation_dirty_ = true;
667  }
668 
669  return *this;
670 }
671 
673 {
675 
676  PangoFont* f = pango_font_map_load_font(
677  pango_cairo_font_map_get_default(),
678  context_.get(),
679  font.get());
680 
681  PangoFontMetrics* m = pango_font_get_metrics(f, nullptr);
682 
683  auto ascent = pango_font_metrics_get_ascent(m);
684  auto descent = pango_font_metrics_get_descent(m);
685 
686  pango_font_metrics_unref(m);
687  g_object_unref(f);
688 
689  return ceil(pango_units_to_double(ascent + descent) / pixel_scale_);
690 }
691 
693 {
694  const int ps = video::get_pixel_scale();
695  if (ps == pixel_scale_) {
696  return;
697  }
698 
700 
701  if (maximum_width_ != -1) {
703  }
704 
705  if (maximum_height_ != -1) {
707  }
708 
709  calculation_dirty_ = true;
710  pixel_scale_ = ps;
711 }
712 
714 {
715  // TODO: clean up this "const everything then mutable everything" mess.
716  // update_pixel_scale() should go in here. But it can't. Because things
717  // are declared const which are not const.
718 
719  if(calculation_dirty_) {
720  assert(layout_ != nullptr);
721 
722  calculation_dirty_ = false;
724  }
725 }
726 
727 PangoRectangle pango_text::calculate_size(PangoLayout& layout) const
728 {
729  PangoRectangle size;
730 
732  pango_layout_set_font_description(&layout, font.get());
733 
735  PangoAttrList *attribute_list = pango_attr_list_new();
736  pango_attr_list_insert(attribute_list
737  , pango_attr_underline_new(PANGO_UNDERLINE_SINGLE));
738 
739  pango_layout_set_attributes(&layout, attribute_list);
740  pango_attr_list_unref(attribute_list);
741  }
742 
743  int maximum_width = 0;
744  if(characters_per_line_ != 0) {
745  PangoFont* f = pango_font_map_load_font(
746  pango_cairo_font_map_get_default(),
747  context_.get(),
748  font.get());
749 
750  PangoFontMetrics* m = pango_font_get_metrics(f, nullptr);
751 
752  int w = pango_font_metrics_get_approximate_char_width(m);
754 
755  maximum_width = ceil(pango_units_to_double(w));
756 
757  pango_font_metrics_unref(m);
758  g_object_unref(f);
759  } else {
760  maximum_width = maximum_width_;
761  }
762 
763  if(maximum_width_ != -1) {
764  maximum_width = std::min(maximum_width, maximum_width_);
765  }
766 
767  pango_layout_set_width(&layout, maximum_width == -1
768  ? -1
769  : maximum_width * PANGO_SCALE);
770  pango_layout_get_pixel_extents(&layout, nullptr, &size);
771 
772  DBG_GUI_L << "pango_text::" << __func__
773  << " text '" << gui2::debug_truncate(text_)
774  << "' maximum_width " << maximum_width
775  << " width " << size.x + size.width
776  << ".";
777 
778  DBG_GUI_L << "pango_text::" << __func__
779  << " text '" << gui2::debug_truncate(text_)
780  << "' font_size " << font_size_
781  << " markedup_text " << markedup_text_
782  << " font_style " << std::hex << font_style_ << std::dec
783  << " maximum_width " << maximum_width
784  << " maximum_height " << maximum_height_
785  << " result " << size
786  << ".";
787 
788  if(maximum_width != -1 && size.x + size.width > maximum_width) {
789  DBG_GUI_L << "pango_text::" << __func__
790  << " text '" << gui2::debug_truncate(text_)
791  << " ' width " << size.x + size.width
792  << " greater as the wanted maximum of " << maximum_width
793  << ".";
794  }
795 
796  // The maximum height is handled here instead of using the library - see the comments in set_maximum_height()
797  if(maximum_height_ != -1 && size.y + size.height > maximum_height_) {
798  DBG_GUI_L << "pango_text::" << __func__
799  << " text '" << gui2::debug_truncate(text_)
800  << " ' height " << size.y + size.height
801  << " greater as the wanted maximum of " << maximum_height_
802  << ".";
803  size.height = maximum_height_ - std::max(0, size.y);
804  }
805 
806  return size;
807 }
808 
809 /***
810  * Inverse table
811  *
812  * Holds a high-precision inverse for each number i, that is, a number x such that x * i / 256 is close to 255.
813  */
815 {
816  unsigned values[256] {};
817 
818  constexpr inverse_table()
819  {
820  values[0] = 0;
821  for (int i = 1; i < 256; ++i) {
822  values[i] = (255 * 256) / i;
823  }
824  }
825 
826  unsigned operator[](uint8_t i) const { return values[i]; }
827 };
828 
829 static constexpr inverse_table inverse_table_;
830 
831 /***
832  * Helper function for un-premultiplying alpha
833  * Div should be the high-precision inverse for the alpha value.
834  */
835 static void unpremultiply(uint8_t & value, const unsigned div) {
836  unsigned temp = (value * div) / 256u;
837  // Note: It's always the case that alpha * div < 256 if div is the inverse
838  // for alpha, so if cairo is computing premultiplied alpha by rounding down,
839  // this min is not necessary. However, if cairo generates illegal output,
840  // the min may be selected.
841  // It's probably not worth removing the min, since branch prediction will
842  // make it essentially free if one of the branches is never actually
843  // selected.
844  value = std::min(255u, temp);
845 }
846 
847 /**
848  * Converts from cairo-format ARGB32 premultiplied alpha to plain alpha.
849  * @param c a uint32 representing the color
850  */
851 static void from_cairo_format(uint32_t & c)
852 {
853  uint8_t a = (c >> 24) & 0xff;
854  uint8_t r = (c >> 16) & 0xff;
855  uint8_t g = (c >> 8) & 0xff;
856  uint8_t b = c & 0xff;
857 
858  const unsigned div = inverse_table_[a];
859  unpremultiply(r, div);
860  unpremultiply(g, div);
861  unpremultiply(b, div);
862 
863  c = (static_cast<uint32_t>(a) << 24) | (static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | static_cast<uint32_t>(b);
864 }
865 
866 void pango_text::render(PangoLayout& layout, const SDL_Rect& viewport, const unsigned stride)
867 {
868  cairo_format_t format = CAIRO_FORMAT_ARGB32;
869 
870  uint8_t* buffer = &surface_buffer_[0];
871 
872  std::unique_ptr<cairo_surface_t, std::function<void(cairo_surface_t*)>> cairo_surface(
873  cairo_image_surface_create_for_data(buffer, format, viewport.w, viewport.h, stride), cairo_surface_destroy);
874  std::unique_ptr<cairo_t, std::function<void(cairo_t*)>> cr(cairo_create(cairo_surface.get()), cairo_destroy);
875 
876  if(cairo_status(cr.get()) == CAIRO_STATUS_INVALID_SIZE) {
877  throw std::length_error("Text is too long to render");
878  }
879 
880  // The top-left of the text, which can be outside the area to be rendered
881  cairo_move_to(cr.get(), -viewport.x, -viewport.y);
882 
883  //
884  // TODO: the outline may be slightly cut off around certain text if it renders too
885  // close to the surface's edge. That causes the outline to extend just slightly
886  // outside the surface's borders. I'm not sure how best to deal with this. Obviously,
887  // we want to increase the surface size, but we also don't want to invalidate all
888  // the placement and size calculations. Thankfully, it's not very noticeable.
889  //
890  // -- vultraz, 2018-03-07
891  //
892  if(add_outline_) {
893  // Add a path to the cairo context tracing the current text.
894  pango_cairo_layout_path(cr.get(), &layout);
895 
896  // Set color for background outline (black).
897  cairo_set_source_rgba(cr.get(), 0.0, 0.0, 0.0, 1.0);
898 
899  cairo_set_line_join(cr.get(), CAIRO_LINE_JOIN_ROUND);
900  cairo_set_line_width(cr.get(), 3.0); // Adjust as necessary
901 
902  // Stroke path to draw outline.
903  cairo_stroke(cr.get());
904  }
905 
906  // Set main text color.
907  cairo_set_source_rgba(cr.get(),
908  foreground_color_.r / 255.0,
909  foreground_color_.g / 255.0,
910  foreground_color_.b / 255.0,
911  foreground_color_.a / 255.0
912  );
913 
914  pango_cairo_show_layout(cr.get(), &layout);
915 }
916 
918 {
919  return create_surface({0, 0, rect_.x + rect_.width, rect_.y + rect_.height});
920 }
921 
922 surface pango_text::create_surface(const SDL_Rect& viewport)
923 {
924  assert(layout_.get());
925 
926  cairo_format_t format = CAIRO_FORMAT_ARGB32;
927  const int stride = cairo_format_stride_for_width(format, viewport.w);
928 
929  // The width and stride can be zero if the text is empty or the stride can be negative to indicate an error from
930  // Cairo. Width isn't tested here because it's implied by stride.
931  if(stride <= 0 || viewport.h <= 0) {
932  surface_buffer_.clear();
933  return nullptr;
934  }
935 
936  DBG_FT << "creating new text surface";
937 
938  // Check to prevent arithmetic overflow when calculating (stride * height).
939  // The size of the viewport should already provide a far lower limit on the
940  // maximum size, but this is left in as a sanity check.
941  if(viewport.h > std::numeric_limits<int>::max() / stride) {
942  throw std::length_error("Text is too long to render");
943  }
944 
945  // Resize buffer appropriately and set all pixel values to 0.
946  surface_buffer_.assign(viewport.h * stride, 0);
947 
948  // Try rendering the whole text in one go. If this throws a length_error
949  // then leave it to the caller to handle; one reason it may throw is that
950  // cairo surfaces are limited to approximately 2**15 pixels in height.
951  render(*layout_, viewport, stride);
952 
953  // The cairo surface is in CAIRO_FORMAT_ARGB32 which uses
954  // pre-multiplied alpha. SDL doesn't use that so the pixels need to be
955  // decoded again.
956  for(int y = 0; y < viewport.h; ++y) {
957  uint32_t* pixels = reinterpret_cast<uint32_t*>(&surface_buffer_[y * stride]);
958  for(int x = 0; x < viewport.w; ++x) {
959  from_cairo_format(pixels[x]);
960  }
961  }
962 
963  return SDL_CreateRGBSurfaceWithFormatFrom(
964  &surface_buffer_[0], viewport.w, viewport.h, 32, stride, SDL_PIXELFORMAT_ARGB8888);
965 }
966 
967 bool pango_text::set_markup(std::string_view text, PangoLayout& layout)
968 {
969  char* raw_text;
970  std::string semi_escaped;
971  bool valid = validate_markup(text, &raw_text, semi_escaped);
972  if(semi_escaped != "") {
973  text = semi_escaped;
974  }
975 
976  if(valid) {
977  if(link_aware_) {
978  std::string formatted_text = format_links(text);
979  pango_layout_set_markup(&layout, formatted_text.c_str(), formatted_text.size());
980  } else {
981  pango_layout_set_markup(&layout, text.data(), text.size());
982  }
983  } else {
984  ERR_GUI_L << "pango_text::" << __func__
985  << " text '" << text
986  << "' has broken markup, set to normal text.";
987  set_text(_("The text contains invalid Pango markup: ") + std::string(text), false);
988  }
989 
990  return valid;
991 }
992 
993 /**
994  * Replaces all instances of URLs in a given string with formatted links
995  * and returns the result.
996  */
997 std::string pango_text::format_links(std::string_view text) const
998 {
999  static const std::string delim = " \n\r\t";
1000  std::ostringstream result;
1001 
1002  std::size_t tok_start = 0;
1003  for(std::size_t pos = 0; pos < text.length(); ++pos) {
1004  if(delim.find(text[pos]) == std::string::npos) {
1005  continue;
1006  }
1007 
1008  if(const auto tok_length = pos - tok_start) {
1009  // Token starts from after the last delimiter up to (but not including) this delimiter
1010  auto token = text.substr(tok_start, tok_length);
1011  if(looks_like_url(token)) {
1012  result << format_as_link(std::string{token}, link_color_);
1013  } else {
1014  result << token;
1015  }
1016  }
1017 
1018  result << text[pos];
1019  tok_start = pos + 1;
1020  }
1021 
1022  // Deal with the remainder token
1023  if(tok_start < text.length()) {
1024  auto token = text.substr(tok_start);
1025  if(looks_like_url(token)) {
1026  result << format_as_link(std::string{token}, link_color_);
1027  } else {
1028  result << token;
1029  }
1030  }
1031 
1032  return result.str();
1033 }
1034 
1035 bool pango_text::validate_markup(std::string_view text, char** raw_text, std::string& semi_escaped) const
1036 {
1037  if(pango_parse_markup(text.data(), text.size(),
1038  0, nullptr, raw_text, nullptr, nullptr)) {
1039  return true;
1040  }
1041 
1042  /*
1043  * The markup is invalid. Try to recover.
1044  *
1045  * The pango engine tested seems to accept stray single quotes »'« and
1046  * double quotes »"«. Stray ampersands »&« seem to give troubles.
1047  * So only try to recover from broken ampersands, by simply replacing them
1048  * with the escaped version.
1049  */
1050  semi_escaped = semi_escape_text(std::string(text));
1051 
1052  /*
1053  * If at least one ampersand is replaced the semi-escaped string
1054  * is longer than the original. If this isn't the case then the
1055  * markup wasn't (only) broken by ampersands in the first place.
1056  */
1057  if(text.size() == semi_escaped.size()
1058  || !pango_parse_markup(semi_escaped.c_str(), semi_escaped.size()
1059  , 0, nullptr, raw_text, nullptr, nullptr)) {
1060 
1061  /* Fixing the ampersands didn't work. */
1062  return false;
1063  }
1064 
1065  /* Replacement worked, still warn the user about the error. */
1066  WRN_GUI_L << "pango_text::" << __func__
1067  << " text '" << text
1068  << "' has unescaped ampersands '&', escaped them.";
1069 
1070  return true;
1071 }
1072 
1073 void pango_text::copy_layout_properties(PangoLayout& src, PangoLayout& dst)
1074 {
1075  pango_layout_set_alignment(&dst, pango_layout_get_alignment(&src));
1076  pango_layout_set_height(&dst, pango_layout_get_height(&src));
1077  pango_layout_set_ellipsize(&dst, pango_layout_get_ellipsize(&src));
1078 }
1079 
1080 std::vector<std::string> pango_text::get_lines() const
1081 {
1082  this->recalculate();
1083 
1084  PangoLayout* const layout = layout_.get();
1085  std::vector<std::string> res;
1086  int count = pango_layout_get_line_count(layout);
1087 
1088  if(count < 1) {
1089  return res;
1090  }
1091 
1092  using layout_iterator = std::unique_ptr<PangoLayoutIter, std::function<void(PangoLayoutIter*)>>;
1093  layout_iterator i{pango_layout_get_iter(layout), pango_layout_iter_free};
1094 
1095  res.reserve(count);
1096 
1097  do {
1098  PangoLayoutLine* ll = pango_layout_iter_get_line_readonly(i.get());
1099  const char* begin = &pango_layout_get_text(layout)[ll->start_index];
1100  res.emplace_back(begin, ll->length);
1101  } while(pango_layout_iter_next_line(i.get()));
1102 
1103  return res;
1104 }
1105 
1106 PangoLayoutLine* pango_text::get_line(int index)
1107 {
1108  return pango_layout_get_line_readonly(layout_.get(), index);
1109 }
1110 
1111 int pango_text::get_line_num_from_offset(const unsigned offset)
1112 {
1113  int line_num = 0;
1114  pango_layout_index_to_line_x(layout_.get(), offset, 0, &line_num, nullptr);
1115  return line_num;
1116 }
1117 
1119 {
1120  static pango_text text_renderer;
1121  return text_renderer;
1122 }
1123 
1125 {
1126  // Reset metrics to defaults
1127  return get_text_renderer()
1128  .set_family_class(fclass)
1129  .set_font_style(style)
1132 }
1133 
1134 } // 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:446
pango_text & set_font_style(const FONT_STYLE font_style)
Definition: text.cpp:529
PangoEllipsizeMode ellipse_mode_
The way too long text is shown depends on this mode.
Definition: text.hpp:415
void set_highlight_area(const unsigned start_offset, const unsigned end_offset, const color_t &color)
Mark a specific portion of text for highlighting.
Definition: text.cpp:436
void add_attribute_weight(const unsigned start_offset, const unsigned end_offset, PangoWeight weight)
Definition: text.cpp:338
static void copy_layout_properties(PangoLayout &src, PangoLayout &dst)
Definition: text.cpp:1073
bool add_outline_
Whether to add an outline effect.
Definition: text.hpp:376
bool validate_markup(std::string_view text, char **raw_text, std::string &semi_escaped) const
Definition: text.cpp:1035
bool set_markup(std::string_view text, PangoLayout &layout)
Sets the markup'ed text.
Definition: text.cpp:967
pango_text & set_maximum_length(const std::size_t maximum_length)
Definition: text.cpp:630
void render(PangoLayout &layout, const SDL_Rect &viewport, const unsigned stride)
This is part of create_surface(viewport).
Definition: text.cpp:866
unsigned insert_text(const unsigned offset, const std::string &text)
Inserts UTF-8 text.
Definition: text.cpp:165
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:917
PangoAlignment alignment_
The alignment of the text.
Definition: text.hpp:418
PangoRectangle rect_
Definition: text.hpp:343
int maximum_height_
The maximum height of the text.
Definition: text.hpp:412
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:564
color_t link_color_
The color to render links in.
Definition: text.hpp:361
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:1111
void recalculate() const
Recalculates the text layout.
Definition: text.cpp:713
color_t foreground_color_
The foreground color.
Definition: text.hpp:373
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:341
bool link_aware_
Are hyperlinks in the text marked-up, and will get_link return them.
Definition: text.hpp:352
pango_text & set_foreground_color(const color_t &color)
Definition: text.cpp:539
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:997
PangoLayoutLine * get_line(int index)
Get a specific line from the pango layout.
Definition: text.cpp:1106
unsigned characters_per_line_
The number of characters per line.
Definition: text.hpp:404
bool markedup_text_
Does the text contain pango markup? If different render routines must be used.
Definition: text.hpp:349
void add_attribute_font_family(const unsigned start_offset, const unsigned end_offset, std::string family)
Definition: text.cpp:417
pango_text & set_family_class(font::family_class fclass)
Definition: text.cpp:507
font::family_class font_class_
The font family class used.
Definition: text.hpp:364
std::vector< std::string > get_lines() const
Retrieves a list of strings with contents for each rendered line.
Definition: text.cpp:1080
void add_attribute_fg_color(const unsigned start_offset, const unsigned end_offset, const color_t &color)
Definition: text.cpp:394
void update_pixel_scale()
Update pixel scale, if necessary.
Definition: text.cpp:692
unsigned font_size_
The font size to draw.
Definition: text.hpp:367
void add_attribute_size(const unsigned start_offset, const unsigned end_offset, int size)
Definition: text.cpp:320
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:491
pango_text & set_add_outline(bool do_add)
Definition: text.cpp:662
unsigned attribute_end_offset_
Definition: text.hpp:436
pango_text & set_ellipse_mode(const PangoEllipsizeMode ellipse_mode)
Definition: text.cpp:600
PangoRectangle calculate_size(PangoLayout &layout) const
Calculates surface size.
Definition: text.cpp:727
color_t highlight_color_
Definition: text.hpp:437
void add_attribute_underline(const unsigned start_offset, const unsigned end_offset, PangoUnderline underline)
Definition: text.cpp:375
pango_text & set_alignment(const PangoAlignment alignment)
Definition: text.cpp:620
std::string text_
The text to draw (stored as UTF-8).
Definition: text.hpp:346
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:430
std::unique_ptr< PangoLayout, std::function< void(void *)> > layout_
Definition: text.hpp:342
pango_text & set_font_size(unsigned font_size)
Definition: text.cpp:517
pango_text & set_link_aware(bool b)
Definition: text.cpp:643
FONT_STYLE font_style_
The style of the font, this is an orred mask of the font flags.
Definition: text.hpp:370
unsigned attribute_start_offset_
Definition: text.hpp:435
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:462
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)
Definition: text.cpp:356
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:421
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:575
pango_text & set_maximum_width(int width)
Definition: text.cpp:548
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:443
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:652
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()
Clear all attributes.
Definition: text.cpp:457
const std::string & text() const
Definition: text.hpp:288
std::size_t length_
Length of the text.
Definition: text.hpp:433
int get_max_glyph_height() const
Returns the maximum glyph height of a font, in drawing coordinates.
Definition: text.cpp:672
int maximum_width_
The maximum width of the text.
Definition: text.hpp:386
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:965
int w
static std::string _(const char *str)
Definition: gettext.hpp:93
Define the common log macros for the gui toolkit.
#define DBG_GUI_L
Definition: log.hpp:55
#define ERR_GUI_L
Definition: log.hpp:58
#define WRN_GUI_L
Definition: log.hpp:57
#define DBG_GUI_D
Definition: log.hpp:29
CURSOR_TYPE get()
Definition: cursor.cpp:216
static void layout()
void point(int x, int y)
Draw a single point.
Definition: draw.cpp:202
void rect(const SDL_Rect &rect)
Draw a rectangle.
Definition: draw.cpp:150
void line(int from_x, int from_y, int to_x, int to_y)
Draw a line.
Definition: draw.cpp:180
Collection of helper functions relating to Pango formatting.
family_class
Font classes for get_font_families().
@ FONT_SANS_SERIF
int get_max_height(unsigned size, font::family_class fclass, pango_text::FONT_STYLE style)
Returns the maximum glyph height of a font, in pixels.
Definition: text.cpp:1124
pango_text & get_text_renderer()
Returns a reference to a static pango_text object.
Definition: text.cpp:1118
constexpr float get_line_spacing_factor()
Definition: text.hpp:557
bool looks_like_url(std::string_view str)
Definition: hyperlink.hpp:27
std::string format_as_link(const std::string &link, color_t color)
Definition: hyperlink.hpp:32
std::string semi_escape_text(const std::string &text)
Definition: escape.hpp:52
const t_string & get_font_families(family_class fclass)
Returns the currently defined fonts.
static void unpremultiply(uint8_t &value, const unsigned div)
Definition: text.cpp:835
static void from_cairo_format(uint32_t &c)
Converts from cairo-format ARGB32 premultiplied alpha to plain alpha.
Definition: text.cpp:851
void flush_texture_cache()
Flush the rendered text cache.
Definition: text.cpp:61
static constexpr inverse_table inverse_table_
Definition: text.cpp:829
std::string_view debug_truncate(std::string_view text)
Returns a truncated version of the text.
Definition: helper.cpp:148
std::size_t index(const std::string &str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:70
std::string & insert(std::string &str, const std::size_t pos, const std::string &insert)
Insert a UTF-8 string at the specified position.
Definition: unicode.cpp:98
std::size_t size(const std::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:85
std::string & truncate(std::string &str, const std::size_t size)
Truncates a UTF-8 string to the specified number of characters.
Definition: unicode.cpp:116
int get_pixel_scale()
Get the current active pixel scale multiplier.
Definition: video.cpp:483
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:816
constexpr inverse_table()
Definition: text.cpp:818
unsigned operator[](uint8_t i) const
Definition: text.cpp:826
Holds a 2D point.
Definition: point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:47
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