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