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