The Battle for Wesnoth  1.17.4+dev
text.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2022
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  // The maximum height is handled in this class' calculate_size() method.
402  //
403  // Although we also pass it to PangoLayout if multiline is true, the documentation of pango_layout_set_height
404  // makes me wonder whether we should avoid that function completely. For example, "at least one line is included
405  // in each paragraph regardless" and "may be changed in future, file a bug if you rely on the current behavior".
406  pango_layout_set_height(layout_.get(), !multiline ? -1 : height * PANGO_SCALE);
407  maximum_height_ = height;
408  calculation_dirty_ = true;
409  surface_dirty_ = true;
410  }
411 
412  return *this;
413 }
414 
415 pango_text& pango_text::set_ellipse_mode(const PangoEllipsizeMode ellipse_mode)
416 {
417  if(ellipse_mode != ellipse_mode_) {
418  // assert(context_);
419 
420  pango_layout_set_ellipsize(layout_.get(), ellipse_mode);
421  ellipse_mode_ = ellipse_mode;
422  calculation_dirty_ = true;
423  surface_dirty_ = true;
424  }
425 
426  // According to the docs of pango_layout_set_height, the behavior is undefined if a height other than -1 is combined
427  // with PANGO_ELLIPSIZE_NONE. Wesnoth's code currently always calls set_ellipse_mode after set_maximum_height, so do
428  // the cleanup here. The code in calculate_size() will still apply the maximum height after Pango's calculations.
429  if(ellipse_mode_ == PANGO_ELLIPSIZE_NONE) {
430  pango_layout_set_height(layout_.get(), -1);
431  }
432 
433  return *this;
434 }
435 
436 pango_text &pango_text::set_alignment(const PangoAlignment alignment)
437 {
438  if (alignment != alignment_) {
439  pango_layout_set_alignment(layout_.get(), alignment);
440  alignment_ = alignment;
441  surface_dirty_ = true;
442  }
443 
444  return *this;
445 }
446 
447 pango_text& pango_text::set_maximum_length(const std::size_t maximum_length)
448 {
449  if(maximum_length != maximum_length_) {
450  maximum_length_ = maximum_length;
451  if(length_ > maximum_length_) {
452  std::string tmp = text_;
453  this->set_text(utf8::truncate(tmp, maximum_length_), false);
454  }
455  }
456 
457  return *this;
458 }
459 
461 {
462  if (link_aware_ != b) {
463  calculation_dirty_ = true;
464  surface_dirty_ = true;
465  link_aware_ = b;
466  }
467  return *this;
468 }
469 
471 {
472  if(color != link_color_) {
473  link_color_ = color;
474  calculation_dirty_ = true;
475  surface_dirty_ = true;
476  }
477 
478  return *this;
479 }
480 
482 {
483  if(do_add != add_outline_) {
484  add_outline_ = do_add;
485  //calculation_dirty_ = true;
486  surface_dirty_ = true;
487  }
488 
489  return *this;
490 }
491 
493 {
495 
496  PangoFont* f = pango_font_map_load_font(
497  pango_cairo_font_map_get_default(),
498  context_.get(),
499  font.get());
500 
501  PangoFontMetrics* m = pango_font_get_metrics(f, nullptr);
502 
503  auto ascent = pango_font_metrics_get_ascent(m);
504  auto descent = pango_font_metrics_get_descent(m);
505 
506  pango_font_metrics_unref(m);
507  g_object_unref(f);
508 
509  return ceil(pango_units_to_double(ascent + descent));
510 }
511 
513 {
514  if(calculation_dirty_) {
515  assert(layout_ != nullptr);
516 
517  calculation_dirty_ = false;
518  surface_dirty_ = true;
519 
521  }
522 }
523 
524 PangoRectangle pango_text::calculate_size(PangoLayout& layout) const
525 {
526  PangoRectangle size;
527 
529  pango_layout_set_font_description(&layout, font.get());
530 
531  if(font_style_ & pango_text::STYLE_UNDERLINE) {
532  PangoAttrList *attribute_list = pango_attr_list_new();
533  pango_attr_list_insert(attribute_list
534  , pango_attr_underline_new(PANGO_UNDERLINE_SINGLE));
535 
536  pango_layout_set_attributes(&layout, attribute_list);
537  pango_attr_list_unref(attribute_list);
538  }
539 
540  int maximum_width = 0;
541  if(characters_per_line_ != 0) {
542  PangoFont* f = pango_font_map_load_font(
543  pango_cairo_font_map_get_default(),
544  context_.get(),
545  font.get());
546 
547  PangoFontMetrics* m = pango_font_get_metrics(f, nullptr);
548 
549  int w = pango_font_metrics_get_approximate_char_width(m);
551 
552  maximum_width = ceil(pango_units_to_double(w));
553 
554  pango_font_metrics_unref(m);
555  g_object_unref(f);
556  } else {
557  maximum_width = maximum_width_;
558  }
559 
560  if(maximum_width_ != -1) {
561  maximum_width = std::min(maximum_width, maximum_width_);
562  }
563 
564  pango_layout_set_width(&layout, maximum_width == -1
565  ? -1
566  : maximum_width * PANGO_SCALE);
567  pango_layout_get_pixel_extents(&layout, nullptr, &size);
568 
569  DBG_GUI_L << "pango_text::" << __func__
570  << " text '" << gui2::debug_truncate(text_)
571  << "' maximum_width " << maximum_width
572  << " width " << size.x + size.width
573  << ".\n";
574 
575  DBG_GUI_L << "pango_text::" << __func__
576  << " text '" << gui2::debug_truncate(text_)
577  << "' font_size " << font_size_
578  << " markedup_text " << markedup_text_
579  << " font_style " << std::hex << font_style_ << std::dec
580  << " maximum_width " << maximum_width
581  << " maximum_height " << maximum_height_
582  << " result " << size
583  << ".\n";
584 
585  if(maximum_width != -1 && size.x + size.width > maximum_width) {
586  DBG_GUI_L << "pango_text::" << __func__
587  << " text '" << gui2::debug_truncate(text_)
588  << " ' width " << size.x + size.width
589  << " greater as the wanted maximum of " << maximum_width
590  << ".\n";
591  }
592 
593  // The maximum height is handled here instead of using the library - see the comments in set_maximum_height()
594  if(maximum_height_ != -1 && size.y + size.height > maximum_height_) {
595  DBG_GUI_L << "pango_text::" << __func__
596  << " text '" << gui2::debug_truncate(text_)
597  << " ' height " << size.y + size.height
598  << " greater as the wanted maximum of " << maximum_height_
599  << ".\n";
600  size.height = maximum_height_ - std::max(0, size.y);
601  }
602 
603  return size;
604 }
605 
606 /***
607  * Inverse table
608  *
609  * Holds a high-precision inverse for each number i, that is, a number x such that x * i / 256 is close to 255.
610  */
612 {
613  unsigned values[256];
614 
616  {
617  values[0] = 0;
618  for (int i = 1; i < 256; ++i) {
619  values[i] = (255 * 256) / i;
620  }
621  }
622 
623  unsigned operator[](uint8_t i) const { return values[i]; }
624 };
625 
627 
628 /***
629  * Helper function for un-premultiplying alpha
630  * Div should be the high-precision inverse for the alpha value.
631  */
632 static void unpremultiply(uint8_t & value, const unsigned div) {
633  unsigned temp = (value * div) / 256u;
634  // Note: It's always the case that alpha * div < 256 if div is the inverse
635  // for alpha, so if cairo is computing premultiplied alpha by rounding down,
636  // this min is not necessary. However, if cairo generates illegal output,
637  // the min may be selected.
638  // It's probably not worth removing the min, since branch prediction will
639  // make it essentially free if one of the branches is never actually
640  // selected.
641  value = std::min(255u, temp);
642 }
643 
644 /**
645  * Converts from cairo-format ARGB32 premultiplied alpha to plain alpha.
646  * @param c a uint32 representing the color
647  */
648 static void from_cairo_format(uint32_t & c)
649 {
650  uint8_t a = (c >> 24) & 0xff;
651  uint8_t r = (c >> 16) & 0xff;
652  uint8_t g = (c >> 8) & 0xff;
653  uint8_t b = c & 0xff;
654 
655  const unsigned div = inverse_table_[a];
656  unpremultiply(r, div);
657  unpremultiply(g, div);
658  unpremultiply(b, div);
659 
660 #ifdef _WIN32
661  // Grayscale AA with ClearType results in wispy unreadable text because of gamma issues
662  // that would normally be solved by rendering directly onto the destination surface without
663  // alpha blending. However, since the current game engine design would never allow us to do
664  // that, we work around that by increasing alpha at the expense of AA accuracy (which is
665  // not particularly noticeable if you don't know what you're looking for anyway).
666  if(a < 255) {
667  a = std::clamp<unsigned>(unsigned(a) * 1.75, 0, 255);
668  }
669 #endif
670 
671  c = (static_cast<uint32_t>(a) << 24) | (static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | static_cast<uint32_t>(b);
672 }
673 
674 void pango_text::render(PangoLayout& layout, const SDL_Rect& viewport, const unsigned stride)
675 {
676  cairo_format_t format = CAIRO_FORMAT_ARGB32;
677 
678  uint8_t* buffer = &surface_buffer_[0];
679 
680  std::unique_ptr<cairo_surface_t, std::function<void(cairo_surface_t*)>> cairo_surface(
681  cairo_image_surface_create_for_data(buffer, format, viewport.w, viewport.h, stride), cairo_surface_destroy);
682  std::unique_ptr<cairo_t, std::function<void(cairo_t*)>> cr(cairo_create(cairo_surface.get()), cairo_destroy);
683 
684  if(cairo_status(cr.get()) == CAIRO_STATUS_INVALID_SIZE) {
685  throw std::length_error("Text is too long to render");
686  }
687 
688  // The top-left of the text, which can be outside the area to be rendered
689  cairo_move_to(cr.get(), -viewport.x, -viewport.y);
690 
691  //
692  // TODO: the outline may be slightly cut off around certain text if it renders too
693  // close to the surface's edge. That causes the outline to extend just slightly
694  // outside the surface's borders. I'm not sure how best to deal with this. Obviously,
695  // we want to increase the surface size, but we also don't want to invalidate all
696  // the placement and size calculations. Thankfully, it's not very noticeable.
697  //
698  // -- vultraz, 2018-03-07
699  //
700  if(add_outline_) {
701  // Add a path to the cairo context tracing the current text.
702  pango_cairo_layout_path(cr.get(), &layout);
703 
704  // Set color for background outline (black).
705  cairo_set_source_rgba(cr.get(), 0.0, 0.0, 0.0, 1.0);
706 
707  cairo_set_line_join(cr.get(), CAIRO_LINE_JOIN_ROUND);
708  cairo_set_line_width(cr.get(), 3.0); // Adjust as necessary
709 
710  // Stroke path to draw outline.
711  cairo_stroke(cr.get());
712  }
713 
714  // Set main text color.
715  cairo_set_source_rgba(cr.get(),
716  foreground_color_.r / 255.0,
717  foreground_color_.g / 255.0,
718  foreground_color_.b / 255.0,
719  foreground_color_.a / 255.0
720  );
721 
722  pango_cairo_show_layout(cr.get(), &layout);
723 }
724 
725 void pango_text::rerender(const SDL_Rect& viewport)
726 {
727  if(surface_dirty_ || !SDL_RectEquals(&rendered_viewport_, &viewport)) {
728  assert(layout_.get());
729 
730  this->recalculate();
731  surface_dirty_ = false;
732  rendered_viewport_ = viewport;
733 
734  cairo_format_t format = CAIRO_FORMAT_ARGB32;
735  const int stride = cairo_format_stride_for_width(format, viewport.w);
736 
737  // The width and stride can be zero if the text is empty or the stride can be negative to indicate an error from
738  // Cairo. Width isn't tested here because it's implied by stride.
739  if(stride <= 0 || viewport.h <= 0) {
740  surface_ = surface(0, 0);
741  surface_buffer_.clear();
742  return;
743  }
744 
745  // Check to prevent arithmetic overflow when calculating (stride * height).
746  // The size of the viewport should already provide a far lower limit on the
747  // maximum size, but this is left in as a sanity check.
748  if(viewport.h > std::numeric_limits<int>::max() / stride) {
749  throw std::length_error("Text is too long to render");
750  }
751 
752  // Resize buffer appropriately and set all pixel values to 0.
753  surface_ = nullptr; // Don't leave a dangling pointer to the old buffer
754  surface_buffer_.assign(viewport.h * stride, 0);
755 
756  // Try rendering the whole text in one go. If this throws a length_error
757  // then leave it to the caller to handle; one reason it may throw is that
758  // cairo surfaces are limited to approximately 2**15 pixels in height.
759  render(*layout_, viewport, stride);
760 
761  // The cairo surface is in CAIRO_FORMAT_ARGB32 which uses
762  // pre-multiplied alpha. SDL doesn't use that so the pixels need to be
763  // decoded again.
764  for(int y = 0; y < viewport.h; ++y) {
765  uint32_t* pixels = reinterpret_cast<uint32_t*>(&surface_buffer_[y * stride]);
766  for(int x = 0; x < viewport.w; ++x) {
767  from_cairo_format(pixels[x]);
768  }
769  }
770 
771  surface_ = SDL_CreateRGBSurfaceWithFormatFrom(
772  &surface_buffer_[0], viewport.w, viewport.h, 32, stride, SDL_PIXELFORMAT_ARGB8888);
773  }
774 }
775 
776 bool pango_text::set_markup(std::string_view text, PangoLayout& layout) {
777  char* raw_text;
778  std::string semi_escaped;
779  bool valid = validate_markup(text, &raw_text, semi_escaped);
780  if(semi_escaped != "") {
781  text = semi_escaped;
782  }
783 
784  if(valid) {
785  if(link_aware_) {
786  std::string formatted_text = format_links(text);
787  pango_layout_set_markup(&layout, formatted_text.c_str(), formatted_text.size());
788  } else {
789  pango_layout_set_markup(&layout, text.data(), text.size());
790  }
791  } else {
792  ERR_GUI_L << "pango_text::" << __func__
793  << " text '" << text
794  << "' has broken markup, set to normal text.\n";
795  set_text(_("The text contains invalid Pango markup: ") + std::string(text), false);
796  }
797 
798  return valid;
799 }
800 
801 /**
802  * Replaces all instances of URLs in a given string with formatted links
803  * and returns the result.
804  */
805 std::string pango_text::format_links(std::string_view text) const
806 {
807  static const std::string delim = " \n\r\t";
808  std::ostringstream result;
809 
810  std::size_t tok_start = 0;
811  for(std::size_t pos = 0; pos < text.length(); ++pos) {
812  if(delim.find(text[pos]) == std::string::npos) {
813  continue;
814  }
815 
816  if(const auto tok_length = pos - tok_start) {
817  // Token starts from after the last delimiter up to (but not including) this delimiter
818  auto token = text.substr(tok_start, tok_length);
819  if(looks_like_url(token)) {
820  result << format_as_link(std::string{token}, link_color_);
821  } else {
822  result << token;
823  }
824  }
825 
826  result << text[pos];
827  tok_start = pos + 1;
828  }
829 
830  // Deal with the remainder token
831  if(tok_start < text.length()) {
832  auto token = text.substr(tok_start);
833  if(looks_like_url(token)) {
834  result << format_as_link(std::string{token}, link_color_);
835  } else {
836  result << token;
837  }
838  }
839 
840  return result.str();
841 }
842 
843 bool pango_text::validate_markup(std::string_view text, char** raw_text, std::string& semi_escaped) const
844 {
845  if(pango_parse_markup(text.data(), text.size(),
846  0, nullptr, raw_text, nullptr, nullptr)) {
847  return true;
848  }
849 
850  /*
851  * The markup is invalid. Try to recover.
852  *
853  * The pango engine tested seems to accept stray single quotes »'« and
854  * double quotes »"«. Stray ampersands »&« seem to give troubles.
855  * So only try to recover from broken ampersands, by simply replacing them
856  * with the escaped version.
857  */
858  semi_escaped = semi_escape_text(std::string(text));
859 
860  /*
861  * If at least one ampersand is replaced the semi-escaped string
862  * is longer than the original. If this isn't the case then the
863  * markup wasn't (only) broken by ampersands in the first place.
864  */
865  if(text.size() == semi_escaped.size()
866  || !pango_parse_markup(semi_escaped.c_str(), semi_escaped.size()
867  , 0, nullptr, raw_text, nullptr, nullptr)) {
868 
869  /* Fixing the ampersands didn't work. */
870  return false;
871  }
872 
873  /* Replacement worked, still warn the user about the error. */
874  WRN_GUI_L << "pango_text::" << __func__
875  << " text '" << text
876  << "' has unescaped ampersands '&', escaped them.\n";
877 
878  return true;
879 }
880 
881 void pango_text::copy_layout_properties(PangoLayout& src, PangoLayout& dst)
882 {
883  pango_layout_set_alignment(&dst, pango_layout_get_alignment(&src));
884  pango_layout_set_height(&dst, pango_layout_get_height(&src));
885  pango_layout_set_ellipsize(&dst, pango_layout_get_ellipsize(&src));
886 }
887 
888 std::vector<std::string> pango_text::get_lines() const
889 {
890  this->recalculate();
891 
892  PangoLayout* const layout = layout_.get();
893  std::vector<std::string> res;
894  int count = pango_layout_get_line_count(layout);
895 
896  if(count < 1) {
897  return res;
898  }
899 
900  using layout_iterator = std::unique_ptr<PangoLayoutIter, std::function<void(PangoLayoutIter*)>>;
901  layout_iterator i{pango_layout_get_iter(layout), pango_layout_iter_free};
902 
903  res.reserve(count);
904 
905  do {
906  PangoLayoutLine* ll = pango_layout_iter_get_line_readonly(i.get());
907  const char* begin = &pango_layout_get_text(layout)[ll->start_index];
908  res.emplace_back(begin, ll->length);
909  } while(pango_layout_iter_next_line(i.get()));
910 
911  return res;
912 }
913 
915 {
916  static pango_text text_renderer;
917  return text_renderer;
918 }
919 
921 {
922  // Reset metrics to defaults
923  return get_text_renderer()
924  .set_family_class(fclass)
925  .set_font_style(style)
926  .set_font_size(size)
928 }
929 
930 } // namespace font
Define the common log macros for the gui toolkit.
void recalculate() const
Recalculates the text layout.
Definition: text.cpp:512
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:632
bool set_markup(std::string_view text, PangoLayout &layout)
Sets the markup&#39;ed text.
Definition: text.cpp:776
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:648
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:888
pango_text & set_link_aware(bool b)
Definition: text.cpp:460
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:725
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:914
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:920
pango_text & set_ellipse_mode(const PangoEllipsizeMode ellipse_mode)
Definition: text.cpp:415
pango_text & set_maximum_length(const std::size_t maximum_length)
Definition: text.cpp:447
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:843
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:436
#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:805
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:507
static const inverse_table inverse_table_
Definition: text.cpp:626
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:623
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:524
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:124
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:492
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:470
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:481
static void copy_layout_properties(PangoLayout &src, PangoLayout &dst)
Definition: text.cpp:881