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