The Battle for Wesnoth  1.13.10+dev
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
text.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2017 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 
37 #include <cassert>
38 #include <cstring>
39 #include <stdexcept>
40 
41 namespace font {
42 
44 #if PANGO_VERSION_CHECK(1,22,0)
45  : context_(pango_font_map_create_context(pango_cairo_font_map_get_default()), g_object_unref)
46 #else
47  : context_(pango_cairo_font_map_create_context((
48  reinterpret_cast<PangoCairoFontMap*>(pango_cairo_font_map_get_default()))), g_object_unref)
49 #endif
50  , layout_(pango_layout_new(context_.get()), g_object_unref)
51  , rect_()
52  , sublayouts_()
53  , surface_()
54  , text_()
55  , markedup_text_(false)
56  , link_aware_(false)
57  , link_color_()
58  , font_class_(font::FONT_SANS_SERIF)
59  , font_size_(14)
60  , font_style_(STYLE_NORMAL)
61  , foreground_color_() // solid white
62  , maximum_width_(-1)
63  , characters_per_line_(0)
64  , maximum_height_(-1)
65  , ellipse_mode_(PANGO_ELLIPSIZE_END)
66  , alignment_(PANGO_ALIGN_LEFT)
67  , maximum_length_(std::string::npos)
68  , calculation_dirty_(true)
69  , length_(0)
70  , surface_dirty_(true)
71  , surface_buffer_()
72 {
73  // With 72 dpi the sizes are the same as with SDL_TTF so hardcoded.
74  pango_cairo_context_set_resolution(context_.get(), 72.0);
75 
76  pango_layout_set_ellipsize(layout_.get(), ellipse_mode_);
77  pango_layout_set_alignment(layout_.get(), alignment_);
78  pango_layout_set_wrap(layout_.get(), PANGO_WRAP_WORD_CHAR);
79 
80  /*
81  * Set the pango spacing a bit bigger since the default is deemed to small
82  * http://www.wesnoth.org/forum/viewtopic.php?p=358832#p358832
83  */
84  pango_layout_set_spacing(layout_.get(), 4 * PANGO_SCALE);
85 
86  cairo_font_options_t *fo = cairo_font_options_create();
87  cairo_font_options_set_hint_style(fo, CAIRO_HINT_STYLE_FULL);
88  cairo_font_options_set_hint_metrics(fo, CAIRO_HINT_METRICS_ON);
89  cairo_font_options_set_antialias(fo, CAIRO_ANTIALIAS_DEFAULT);
90 
91  pango_cairo_context_set_font_options(context_.get(), fo);
92  cairo_font_options_destroy(fo);
93 }
94 
96 {
97  this->rerender();
98  return surface_;
99 }
100 
101 
103 {
104  return this->get_size().x;
105 }
106 
108 {
109  return this->get_size().y;
110 }
111 
113 {
114  this->recalculate();
115 
116  return point(rect_.width, rect_.height);
117 }
118 
120 {
121  this->recalculate();
122 
123  return (pango_layout_is_ellipsized(layout_.get()) != 0);
124 }
125 
126 unsigned pango_text::insert_text(const unsigned offset, const std::string& text)
127 {
128  if (text.empty() || length_ == maximum_length_) {
129  return 0;
130  }
131 
132  // do we really need that assert? utf8::insert will just append in this case, which seems fine
133  assert(offset <= length_);
134 
135  unsigned len = utf8::size(text);
136  if (length_ + len > maximum_length_) {
137  len = maximum_length_ - length_;
138  }
139  const utf8::string insert = text.substr(0, utf8::index(text, len));
140  utf8::string tmp = text_;
141  this->set_text(utf8::insert(tmp, offset, insert), false);
142  // report back how many characters were actually inserted (e.g. to move the cursor selection)
143  return len;
144 }
145 
146 bool pango_text::insert_unicode(const unsigned offset, ucs4::char_t unicode)
147 {
148  return this->insert_unicode(offset, ucs4::string(1, unicode)) == 1;
149 }
150 
151 unsigned pango_text::insert_unicode(const unsigned offset, const ucs4::string& unicode)
152 {
153  const utf8::string insert = unicode_cast<utf8::string>(unicode);
154  return this->insert_text(offset, insert);
155 }
156 
158  const unsigned column, const unsigned line) const
159 {
160  this->recalculate();
161 
162  // First we need to determine the byte offset, if more routines need it it
163  // would be a good idea to make it a separate function.
164  std::unique_ptr<PangoLayoutIter, std::function<void(PangoLayoutIter*)>> itor(
165  pango_layout_get_iter(layout_.get()), pango_layout_iter_free);
166 
167  // Go the wanted line.
168  if(line != 0) {
169  if(pango_layout_get_line_count(layout_.get()) >= static_cast<int>(line)) {
170  return point(0, 0);
171  }
172 
173  for(size_t i = 0; i < line; ++i) {
174  pango_layout_iter_next_line(itor.get());
175  }
176  }
177 
178  // Go the wanted column.
179  for(size_t i = 0; i < column; ++i) {
180  if(!pango_layout_iter_next_char(itor.get())) {
181  // It seems that the documentation is wrong and causes and off by
182  // one error... the result should be false if already at the end of
183  // the data when started.
184  if(i + 1 == column) {
185  break;
186  }
187  // We are beyond data.
188  return point(0, 0);
189  }
190  }
191 
192  // Get the byte offset
193  const int offset = pango_layout_iter_get_index(itor.get());
194 
195  // Convert the byte offset in a position.
196  PangoRectangle rect;
197  pango_layout_get_cursor_pos(layout_.get(), offset, &rect, nullptr);
198 
199  return point(PANGO_PIXELS(rect.x), PANGO_PIXELS(rect.y));
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<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  size_t l = index;
222  while (l > 0 && (d.find(txt.at(l-1)) == std::string::npos)) {
223  --l;
224  }
225 
226  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(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  sublayouts_.clear();
287  if(layout_ == nullptr) {
288  layout_.reset(pango_layout_new(context_.get()));
289  }
290 
292  const std::string narrow = unicode_cast<utf8::string>(wide);
293  if(text != narrow) {
294  ERR_GUI_L << "pango_text::" << __func__
295  << " text '" << text
296  << "' contains invalid utf-8, trimmed the invalid parts.\n";
297  }
298  if(markedup) {
299  if(!this->set_markup(narrow, *layout_)) {
300  return false;
301  }
302  } else {
303  /*
304  * pango_layout_set_text after pango_layout_set_markup might
305  * leave the layout in an undefined state regarding markup so
306  * clear it unconditionally.
307  */
308  pango_layout_set_attributes(layout_.get(), nullptr);
309  pango_layout_set_text(layout_.get(), narrow.c_str(), narrow.size());
310  }
311  text_ = narrow;
312  length_ = wide.size();
313  markedup_text_ = markedup;
314  calculation_dirty_ = true;
315  surface_dirty_ = true;
316  }
317 
318  return true;
319 }
320 
322 {
323  if(fclass != font_class_) {
324  font_class_ = fclass;
325  calculation_dirty_ = true;
326  surface_dirty_ = true;
327  }
328 
329  return *this;
330 }
331 
333 {
334  unsigned int actual_size = preferences::font_scaled(font_size);
335  if(actual_size != font_size_) {
336  font_size_ = actual_size;
337  calculation_dirty_ = true;
338  surface_dirty_ = true;
339  }
340 
341  return *this;
342 }
343 
345 {
346  if(font_style != font_style_) {
347  font_style_ = font_style;
348  calculation_dirty_ = true;
349  surface_dirty_ = true;
350  }
351 
352  return *this;
353 }
354 
356 {
357  if(color != foreground_color_) {
358  foreground_color_ = color;
359  surface_dirty_ = true;
360  }
361 
362  return *this;
363 }
364 
366 {
367  if(width <= 0) {
368  width = -1;
369  }
370 
371  if(width != maximum_width_) {
372  // assert(context_);
373 #if 0
374  /**
375  * todo Adding 4 extra pixels feels a bit hacky.
376  *
377  * For some reason it's needed since the following scenario fails:
378  * - pango_layout_set_width(value)
379  * - pango_layout_get_pixel_extents() -> max_width_1
380  * - pango_layout_set_width(max_width_1)
381  * - pango_layout_get_pixel_extents() -> max_width_2
382  *
383  * Now it can happen max_width_2 < max_width_1. Adding the 4 seems to
384  * "fix" the problem.
385  */
386  pango_layout_set_width(layout_, width == -1
387  ? -1
388  : (width + 4) * PANGO_SCALE);
389 #endif
390  maximum_width_ = width;
391  calculation_dirty_ = true;
392  surface_dirty_ = true;
393  }
394 
395  return *this;
396 }
397 
398 pango_text& pango_text::set_characters_per_line(const unsigned characters_per_line)
399 {
400  if(characters_per_line != characters_per_line_) {
401  characters_per_line_ = characters_per_line;
402 
403  calculation_dirty_ = true;
404  surface_dirty_ = true;
405  }
406 
407  return *this;
408 }
409 
410 pango_text& pango_text::set_maximum_height(int height, bool multiline)
411 {
412  if(height <= 0) {
413  height = -1;
414  multiline = false;
415  }
416 
417  if(height != maximum_height_) {
418  // assert(context_);
419 
420  pango_layout_set_height(layout_.get(), !multiline ? -1 : height * PANGO_SCALE);
421  maximum_height_ = height;
422  calculation_dirty_ = true;
423  surface_dirty_ = true;
424  }
425 
426  return *this;
427 }
428 
429 pango_text& pango_text::set_ellipse_mode(const PangoEllipsizeMode ellipse_mode)
430 {
431  if(ellipse_mode != ellipse_mode_) {
432  // assert(context_);
433 
434  pango_layout_set_ellipsize(layout_.get(), ellipse_mode);
435  ellipse_mode_ = ellipse_mode;
436  calculation_dirty_ = true;
437  surface_dirty_ = true;
438  }
439 
440  return *this;
441 }
442 
443 pango_text &pango_text::set_alignment(const PangoAlignment alignment)
444 {
445  if (alignment != alignment_) {
446  pango_layout_set_alignment(layout_.get(), alignment);
447  alignment_ = alignment;
448  surface_dirty_ = true;
449  }
450 
451  return *this;
452 }
453 
454 pango_text& pango_text::set_maximum_length(const size_t maximum_length)
455 {
456  if(maximum_length != maximum_length_) {
457  maximum_length_ = maximum_length;
458  if(length_ > maximum_length_) {
459  utf8::string tmp = text_;
460  this->set_text(utf8::truncate(tmp, maximum_length_), false);
461  }
462  }
463 
464  return *this;
465 }
466 
468 {
469  if (link_aware_ != b) {
470  calculation_dirty_ = true;
471  surface_dirty_ = true;
472  link_aware_ = b;
473  }
474  return *this;
475 }
476 
478 {
479  if(color != link_color_) {
480  link_color_ = color;
481  calculation_dirty_ = true;
482  surface_dirty_ = true;
483  }
484 
485  return *this;
486 }
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 
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  /*
539  * See set_maximum_width for some more background info as well.
540  * In order to fix the problem first set a width which seems to render
541  * correctly then lower it to fit. For the campaigns the 4 does "the
542  * right thing" for the terrain labels it should use the value 0 to set
543  * the ellipse properly. Need to see whether this is a bug in pango or
544  * a bug in my understanding of the pango api.
545  */
546  int hack = 4;
547  do {
548  pango_layout_set_width(&layout, maximum_width == -1
549  ? -1
550  : (maximum_width + hack) * PANGO_SCALE);
551  pango_layout_get_pixel_extents(&layout, nullptr, &size);
552 
553  DBG_GUI_L << "pango_text::" << __func__
554  << " text '" << gui2::debug_truncate(text_)
555  << "' maximum_width " << maximum_width
556  << " hack " << hack
557  << " width " << size.x + size.width
558  << ".\n";
559 
560  --hack;
561  } while(maximum_width != -1
562  && hack >= 0 && size.x + size.width > maximum_width);
563 
564  DBG_GUI_L << "pango_text::" << __func__
565  << " text '" << gui2::debug_truncate(text_)
566  << "' font_size " << font_size_
567  << " markedup_text " << markedup_text_
568  << " font_style " << std::hex << font_style_ << std::dec
569  << " maximum_width " << maximum_width
570  << " maximum_height " << maximum_height_
571  << " result " << size
572  << ".\n";
573  if(maximum_width != -1 && size.x + size.width > maximum_width) {
574  DBG_GUI_L << "pango_text::" << __func__
575  << " text '" << gui2::debug_truncate(text_)
576  << " ' width " << size.x + size.width
577  << " greater as the wanted maximum of " << maximum_width
578  << ".\n";
579  }
580 
581  return size;
582 }
583 
584 /***
585  * Inverse table
586  *
587  * Holds a high-precision inverse for each number i, that is, a number x such that x * i / 256 is close to 255.
588  */
590 {
591  unsigned values[256];
592 
594  {
595  values[0] = 0;
596  for (int i = 1; i < 256; ++i) {
597  values[i] = (255 * 256) / i;
598  }
599  }
600 
601  unsigned operator[](uint8_t i) const { return values[i]; }
602 };
603 
605 
606 /***
607  * Helper function for un-premultiplying alpha
608  * Div should be the high-precision inverse for the alpha value.
609  */
610 static void unpremultiply(uint8_t & value, const unsigned div) {
611  unsigned temp = (value * div) / 256u;
612  // Note: It's always the case that alpha * div < 256 if div is the inverse
613  // for alpha, so if cairo is computing premultiplied alpha by rounding down,
614  // this min is not necessary. However, if cairo generates illegal output,
615  // the min may be selected.
616  // It's probably not worth removing the min, since branch prediction will
617  // make it essentially free if one of the branches is never actually
618  // selected.
619  value = std::min(255u, temp);
620 }
621 
622 /**
623  * Converts from cairo-format ARGB32 premultiplied alpha to plain alpha.
624  * @param c a uint32 representing the color
625  */
626 static void from_cairo_format(uint32_t & c)
627 {
628  uint8_t a = (c >> 24) & 0xff;
629  uint8_t r = (c >> 16) & 0xff;
630  uint8_t g = (c >> 8) & 0xff;
631  uint8_t b = c & 0xff;
632 
633  const unsigned div = inverse_table_[a];
634  unpremultiply(r, div);
635  unpremultiply(g, div);
636  unpremultiply(b, div);
637 
638  c = (static_cast<uint32_t>(a) << 24) | (static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | static_cast<uint32_t>(b);
639 }
640 
641 void pango_text::render(PangoLayout& layout, const PangoRectangle& rect, const size_t surface_buffer_offset, const unsigned stride)
642 {
643  int width = rect.x + rect.width;
644  int height = rect.y + rect.height;
645  if(maximum_width_ > 0) { width = std::min(width, maximum_width_); }
646  if(maximum_height_ > 0) { height = std::min(height, maximum_height_); }
647 
648  cairo_format_t format = CAIRO_FORMAT_ARGB32;
649 
650  uint8_t* buffer = &surface_buffer_[surface_buffer_offset];
651 
652  std::unique_ptr<cairo_surface_t, std::function<void(cairo_surface_t*)>> cairo_surface(
653  cairo_image_surface_create_for_data(buffer, format, width, height, stride), cairo_surface_destroy);
654  std::unique_ptr<cairo_t, std::function<void(cairo_t*)>> cr(cairo_create(cairo_surface.get()), cairo_destroy);
655 
656  if(cairo_status(cr.get()) == CAIRO_STATUS_INVALID_SIZE) {
657  if(!is_surface_split()) {
658  split_surface();
659 
660  PangoRectangle upper_rect = calculate_size(*sublayouts_[0]);
661  PangoRectangle lower_rect = calculate_size(*sublayouts_[1]);
662 
663  render(*sublayouts_[0], upper_rect, 0u, stride);
664  render(*sublayouts_[1], lower_rect, upper_rect.height * stride, stride);
665 
666  return;
667  } else {
668  // If this occurs in practice, it can be fixed by implementing recursive splitting.
669  throw std::length_error("Text is too long to render");
670  }
671  }
672 
673  /* set color (used for foreground). */
674  cairo_set_source_rgba(cr.get(),
675  foreground_color_.r / 255.0,
676  foreground_color_.g / 255.0,
677  foreground_color_.b / 255.0,
678  foreground_color_.a / 255.0
679  );
680 
681  pango_cairo_show_layout(cr.get(), &layout);
682 }
683 
684 void pango_text::rerender(const bool force)
685 {
686  if(surface_dirty_ || force) {
687  assert(layout_.get());
688 
689  this->recalculate(force);
690  surface_dirty_ = false;
691 
692  int width = rect_.x + rect_.width;
693  int height = rect_.y + rect_.height;
694  if(maximum_width_ > 0) { width = std::min(width, maximum_width_); }
695  if(maximum_height_ > 0) { height = std::min(height, maximum_height_); }
696 
697  cairo_format_t format = CAIRO_FORMAT_ARGB32;
698  const unsigned stride = cairo_format_stride_for_width(format, width);
699 
700  this->create_surface_buffer(stride * height);
701 
702  if (surface_buffer_.empty()) {
704  return;
705  }
706 
707  render(*layout_, rect_, 0u, stride);
708 
709  // The cairo surface is in CAIRO_FORMAT_ARGB32 which uses
710  // pre-multiplied alpha. SDL doesn't use that so the pixels need to be
711  // decoded again.
712  uint32_t * pixels = reinterpret_cast<uint32_t *>(&surface_buffer_[0]);
713 
714  for(int y = 0; y < height; ++y) {
715  for(int x = 0; x < width; ++x) {
716  from_cairo_format(pixels[y * width + x]);
717  }
718  }
719 
720 #if SDL_VERSION_ATLEAST(2, 0, 6)
721  surface_.assign(SDL_CreateRGBSurfaceWithFormatFrom(
722  &surface_buffer_[0], width, height, 32, stride, SDL_PIXELFORMAT_ARGB8888));
723 #else
724  surface_.assign(SDL_CreateRGBSurfaceFrom(
725  &surface_buffer_[0], width, height, 32, stride, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000));
726 #endif
727  }
728 }
729 
730 void pango_text::create_surface_buffer(const size_t size) const
731 {
732  // Clear surface.
733  surface_.assign(nullptr);
734 
735  // Resize buffer appropriately and clear all existing data (essentially sets all pixel values to 0).
736  surface_buffer_.assign(size, 0);
737 }
738 
739 bool pango_text::set_markup(utils::string_view text, PangoLayout& layout) {
740  char* raw_text;
741  std::string semi_escaped;
742  bool valid = validate_markup(text, &raw_text, semi_escaped);
743  if(semi_escaped != "") {
744  text = semi_escaped;
745  }
746 
747  if(valid) {
748  if(link_aware_) {
749  std::vector<std::string> links = find_links(raw_text);
750  std::string final_text = text.to_string();
751  format_links(final_text, links);
752  pango_layout_set_markup(&layout, final_text.c_str(), final_text.size());
753  } else {
754  pango_layout_set_markup(&layout, text.data(), text.size());
755  }
756  } else {
757  ERR_GUI_L << "pango_text::" << __func__
758  << " text '" << text
759  << "' has broken markup, set to normal text.\n";
760  set_text(_("The text contains invalid markup: ") + text.to_string(), false);
761  }
762 
763  return valid;
764 }
765 
766 std::vector<std::string> pango_text::find_links(utils::string_view text) const {
767  const std::string delim = " \n\r\t";
768  std::vector<std::string> links;
769 
770  int last_delim = -1;
771  for (size_t index = 0; index < text.size(); ++index) {
772  if (delim.find(text[index]) != std::string::npos) {
773  // want to include chars from range since last token, dont want to include any delimiters
774  utils::string_view token = text.substr(last_delim + 1, index - last_delim - 1);
775  if(looks_like_url(token)) {
776  links.push_back(token.to_string());
777  }
778  last_delim = index;
779  }
780  }
781  if (last_delim < static_cast<int>(text.size()) - 1) {
782  utils::string_view token = text.substr(last_delim + 1, text.size() - last_delim - 1);
783  if(looks_like_url(token)) {
784  links.push_back(token.to_string());
785  }
786  }
787 
788  return links;
789 }
790 
791 void pango_text::format_links(std::string& text, const std::vector<std::string>& links) const
792 {
793  for(const std::string& link : links) {
794  boost::algorithm::replace_first(text, link, format_as_link(link, link_color_));
795  }
796 }
797 
798 bool pango_text::validate_markup(utils::string_view text, char** raw_text, std::string& semi_escaped) const
799 {
800  if(pango_parse_markup(text.data(), text.size(),
801  0, nullptr, raw_text, nullptr, nullptr)) {
802  return true;
803  }
804 
805  /*
806  * The markup is invalid. Try to recover.
807  *
808  * The pango engine tested seems to accept stray single quotes »'« and
809  * double quotes »"«. Stray ampersands »&« seem to give troubles.
810  * So only try to recover from broken ampersands, by simply replacing them
811  * with the escaped version.
812  */
813  semi_escaped = semi_escape_text(text.to_string());
814 
815  /*
816  * If at least one ampersand is replaced the semi-escaped string
817  * is longer than the original. If this isn't the case then the
818  * markup wasn't (only) broken by ampersands in the first place.
819  */
820  if(text.size() == semi_escaped.size()
821  || !pango_parse_markup(semi_escaped.c_str(), semi_escaped.size()
822  , 0, nullptr, raw_text, nullptr, nullptr)) {
823 
824  /* Fixing the ampersands didn't work. */
825  return false;
826  }
827 
828  /* Replacement worked, still warn the user about the error. */
829  WRN_GUI_L << "pango_text::" << __func__
830  << " text '" << text
831  << "' has unescaped ampersands '&', escaped them.\n";
832 
833  return true;
834 }
835 
837 {
838  auto text_parts = utils::vertical_split(text_);
839 
840  PangoLayout* upper_layout = pango_layout_new(context_.get());
841  PangoLayout* lower_layout = pango_layout_new(context_.get());
842 
843  set_markup(text_parts.first, *upper_layout);
844  set_markup(text_parts.second, *lower_layout);
845 
846  copy_layout_properties(*layout_, *upper_layout);
847  copy_layout_properties(*layout_, *lower_layout);
848 
849  sublayouts_.emplace_back(upper_layout, g_object_unref);
850  sublayouts_.emplace_back(lower_layout, g_object_unref);
851 
852  // Freeing the old layout causes all text to use
853  // default line spacing in the future.
854  // layout_.reset(nullptr);
855 }
856 
857 void pango_text::copy_layout_properties(PangoLayout& src, PangoLayout& dst)
858 {
859  pango_layout_set_alignment(&dst, pango_layout_get_alignment(&src));
860  pango_layout_set_height(&dst, pango_layout_get_height(&src));
861  pango_layout_set_ellipsize(&dst, pango_layout_get_ellipsize(&src));
862 }
863 
864 } // namespace font
Define the common log macros for the gui toolkit.
surface create_neutral_surface(int w, int h)
Definition: utils.cpp:84
std::vector< char_t > string
size_t index(const utf8::string &str, const size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:71
unsigned font_size_
The font size to draw.
Definition: text.hpp:280
bool looks_like_url(utils::string_view str)
Definition: hyperlink.hpp:26
#define DBG_GUI_L
Definition: log.hpp:57
family_class
Font classes for get_font_families().
point get_size() const
Returns the pixel size needed for the text.
Definition: text.cpp:112
bool insert_unicode(const unsigned offset, ucs4::char_t unicode)
Inserts a unicode char.
Definition: text.cpp:146
std::string get_token(const point &position, const char *delimiters=" \n\r\t") const
Gets the largest collection of characters, including the token at position, and not including any cha...
Definition: text.cpp:202
bool set_text(const std::string &text, const bool markedup)
Sets the text to render.
Definition: text.cpp:283
const std::string & text() const
Definition: text.hpp:217
Note: Specific to sdl_ttf.
point get_column_line(const point &position) const
Gets the column of line of the character at the position.
Definition: text.cpp:249
static void unpremultiply(uint8_t &value, const unsigned div)
Definition: text.cpp:610
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
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:626
int maximum_height_
The maximum height of the text.
Definition: text.hpp:322
#define a
unsigned characters_per_line_
The number of characters per line.
Definition: text.hpp:314
ucs4_convert_impl::enableif< TD, typename TS::value_type >::type unicode_cast(const TS &source)
pango_text & set_maximum_length(const size_t maximum_length)
Definition: text.cpp:454
#define WRN_GUI_L
Definition: log.hpp:59
pango_text & set_link_aware(bool b)
Definition: text.cpp:467
int get_width() const
Returns the width needed for the text.
Definition: text.cpp:102
std::basic_string< charT, traits, Allocator > to_string(const Allocator &a=Allocator()) const
std::vector< std::string > find_links(utils::string_view text) const
Definition: text.cpp:766
#define d
font::family_class font_class_
The font family class used.
Definition: text.hpp:277
pango_text & set_font_style(const FONT_STYLE font_style)
Definition: text.cpp:344
uint32_t char_t
pango_text & set_ellipse_mode(const PangoEllipsizeMode ellipse_mode)
Definition: text.cpp:429
std::vector< uint8_t > surface_buffer_
Buffer to store the image on.
Definition: text.hpp:380
Small helper class to make sure the pango font object is destroyed properly.
Definition: font.hpp:23
int x
x coordinate.
Definition: point.hpp:43
pango_text & set_alignment(const PangoAlignment alignment)
Definition: text.cpp:443
bool set_markup(utils::string_view text, PangoLayout &layout)
Sets the markup'ed text.
Definition: text.cpp:739
#define b
int get_height() const
Returns the height needed for the text.
Definition: text.cpp:107
#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
unsigned operator[](uint8_t i) const
Definition: text.cpp:601
std::unique_ptr< PangoLayout, std::function< void(void *)> > layout_
Definition: text.hpp:248
void recalculate(const bool force=false) const
Recalculates the text layout.
Definition: text.cpp:489
unsigned values[256]
Definition: text.cpp:591
int maximum_width_
The maximum width of the text.
Definition: text.hpp:296
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:684
color_t foreground_color_
The foreground color.
Definition: text.hpp:286
pango_text & set_font_size(const unsigned font_size)
Definition: text.cpp:332
int font_scaled(int size)
Definition: general.cpp:451
static const inverse_table inverse_table_
Definition: text.cpp:604
PangoEllipsizeMode ellipse_mode_
The way too long text is shown depends on this mode.
Definition: text.hpp:325
bool validate_markup(utils::string_view text, char **raw_text, std::string &semi_escaped) const
Definition: text.cpp:798
pango_text & set_characters_per_line(const unsigned characters_per_line)
Definition: text.cpp:398
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:262
utf8::string & truncate(utf8::string &str, const size_t size)
Truncates a UTF-8 string to the specified number of characters.
Definition: unicode.cpp:117
bool surface_dirty_
The dirty state of the surface.
Definition: text.hpp:358
PangoRectangle rect_
Definition: text.hpp:249
bool calculation_dirty_
The text has two dirty states:
Definition: text.hpp:340
unsigned insert_text(const unsigned offset, const std::string &text)
Inserts UTF-8 text.
Definition: text.cpp:126
pango_text & set_family_class(font::family_class fclass)
Definition: text.cpp:321
BOOST_CONSTEXPR size_type size() const BOOST_NOEXCEPT
size_t size(const utf8::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:86
std::unique_ptr< PangoContext, std::function< void(void *)> > context_
Definition: text.hpp:247
bool link_aware_
Are hyperlinks in the text marked-up, and will get_link return them.
Definition: text.hpp:265
bool is_truncated() const
Has the text been truncated? This happens if it exceeds max width or height.
Definition: text.cpp:119
std::string debug_truncate(const std::string &text)
Returns a truncated version of the text.
Definition: helper.cpp:125
double g
Definition: astarsearch.cpp:64
PangoRectangle calculate_size(PangoLayout &layout) const
Calculates surface size.
Definition: text.cpp:501
size_t i
Definition: function.cpp:933
FONT_STYLE font_style_
The style of the font, this is an orred mask of the font flags.
Definition: text.hpp:283
point get_cursor_position(const unsigned column, const unsigned line=0) const
Gets the location for the cursor.
Definition: text.cpp:157
size_t maximum_length_
The maximum length of the text.
Definition: text.hpp:331
Holds a 2D point.
Definition: point.hpp:22
pango_text & set_link_color(const color_t &color)
Definition: text.cpp:477
int w
void create_surface_buffer(const size_t size) const
Creates a new buffer.
Definition: text.cpp:730
color_t link_color_
The color to render links in.
Definition: text.hpp:274
Text class.
Definition: text.hpp:75
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:365
void assign(SDL_Surface *surf)
Definition: surface.hpp:46
#define f
void format_links(std::string &text, const std::vector< std::string > &links) const
Definition: text.cpp:791
BOOST_CXX14_CONSTEXPR basic_string_view substr(size_type pos, size_type n=npos) const
bool is_surface_split() const
Definition: text.hpp:426
size_t length_
Length of the text.
Definition: text.hpp:343
utf8::string & insert(utf8::string &str, const size_t pos, const utf8::string &insert)
Insert a UTF-8 string at the specified position.
Definition: unicode.cpp:99
std::vector< std::unique_ptr< PangoLayout, std::function< void(void *)> > > sublayouts_
Definition: text.hpp:252
surface surface_
The SDL surface to render upon used as a cache.
Definition: text.hpp:255
surface & render()
Returns the rendered text.
Definition: text.cpp:95
pango_text & set_maximum_height(int height, bool multiline)
Definition: text.cpp:410
void split_surface()
Splits the text to two Cairo surfaces.
Definition: text.cpp:836
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:259
pango_text & set_foreground_color(const color_t &color)
Definition: text.cpp:355
const int font_size
int y
y coordinate.
Definition: point.hpp:46
BOOST_CONSTEXPR const_pointer data() const BOOST_NOEXCEPT
PangoAlignment alignment_
The alignment of the text.
Definition: text.hpp:328
std::string string
static void copy_layout_properties(PangoLayout &src, PangoLayout &dst)
Definition: text.cpp:857