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