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