The Battle for Wesnoth  1.19.11+dev
text.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2025
3  by Mark de Wever <koraq@xs4all.nl>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #define GETTEXT_DOMAIN "wesnoth-lib"
17 
18 #include "font/text.hpp"
19 
20 #include "font/attributes.hpp"
21 #include "font/cairo.hpp"
22 #include "font/font_config.hpp"
23 
24 #include "font/pango/escape.hpp"
25 #include "font/pango/font.hpp"
26 #include "font/pango/hyperlink.hpp"
28 
29 #include "gettext.hpp"
30 #include "gui/widgets/helper.hpp"
31 #include "gui/core/log.hpp"
32 #include "sdl/point.hpp"
35 #include "video.hpp"
36 
37 #include <cassert>
38 #include <cstring>
39 #include <stdexcept>
40 
41 static lg::log_domain log_font("font");
42 #define DBG_FT LOG_STREAM(debug, log_font)
43 
44 namespace font
45 {
46 
47 namespace
48 {
49 void render_image_shape(cairo_t* cr, PangoAttrShape* pShape, int /* do_path */, void* /* data */)
50 {
51  // NOTE: this data is owned by the underlying SDL_Surface. See add_attribute_image_shape
52  cairo_surface_t* img = static_cast<cairo_surface_t*>(pShape->data);
53 
54  cairo_rel_move_to (cr,
55  pShape->ink_rect.x/PANGO_SCALE,
56  pShape->ink_rect.y/PANGO_SCALE);
57  double x, y;
58  cairo_get_current_point (cr, &x, &y);
59  cairo_translate (cr, x, y);
60  cairo_scale(cr, video::get_pixel_scale(), video::get_pixel_scale());
61 
62  cairo_set_source_surface(cr, img, 0, 0);
63  cairo_rectangle(cr,
64  0,
65  0,
66  pShape->ink_rect.width/PANGO_SCALE,
67  pShape->ink_rect.height/PANGO_SCALE);
68  cairo_fill(cr);
69 }
70 }
71 
73  : context_(pango_font_map_create_context(pango_cairo_font_map_get_default()), g_object_unref)
74  , layout_(pango_layout_new(context_.get()), g_object_unref)
75  , rect_()
76  , text_()
77  , markedup_text_(false)
78  , link_aware_(false)
79  , link_color_()
80  , font_class_(font::family_class::sans_serif)
81  , font_size_(14)
82  , font_style_(STYLE_NORMAL)
83  , foreground_color_() // solid white
84  , add_outline_(false)
85  , maximum_width_(-1)
86  , characters_per_line_(0)
87  , maximum_height_(-1)
88  , ellipse_mode_(PANGO_ELLIPSIZE_END)
89  , alignment_(PANGO_ALIGN_LEFT)
90  , maximum_length_(std::string::npos)
91  , calculation_dirty_(true)
92  , length_(0)
93  , pixel_scale_(1)
94  , surface_buffer_()
95 {
96  // With 72 dpi the sizes are the same as with SDL_TTF so hardcoded.
97  pango_cairo_context_set_resolution(context_.get(), 72.0);
98 
99  pango_layout_set_ellipsize(layout_.get(), ellipse_mode_);
100  pango_layout_set_alignment(layout_.get(), alignment_);
101  pango_layout_set_wrap(layout_.get(), PANGO_WRAP_WORD_CHAR);
102 
103  // TODO: phase this out in favor of a global line height attribute.
104  pango_layout_set_line_spacing(layout_.get(), get_line_spacing_factor());
105 
106  cairo_font_options_t *fo = cairo_font_options_create();
107  cairo_font_options_set_hint_style(fo, CAIRO_HINT_STYLE_FULL);
108  cairo_font_options_set_hint_metrics(fo, CAIRO_HINT_METRICS_ON);
109  cairo_font_options_set_antialias(fo, CAIRO_ANTIALIAS_DEFAULT);
110 
111  pango_cairo_context_set_font_options(context_.get(), fo);
112  cairo_font_options_destroy(fo);
113 
114  pango_cairo_context_set_shape_renderer(context_.get(), render_image_shape, nullptr, nullptr);
115 }
116 
117 texture pango_text::render_texture(const SDL_Rect& viewport)
118 {
119  return with_draw_scale(texture(render_surface(viewport)));
120 }
121 
123 {
124  update_pixel_scale(); // TODO: this should be in recalculate()
125  recalculate();
127 }
128 
129 surface pango_text::render_surface(const SDL_Rect& viewport)
130 {
131  update_pixel_scale(); // TODO: this should be in recalculate()
132  recalculate();
133  return create_surface(viewport);
134 }
135 
137 {
138  texture res(t);
139  res.set_draw_size(to_draw_scale(t.get_raw_size()));
140  return res;
141 }
142 
144 {
145  return (i + pixel_scale_ - 1) / pixel_scale_;
146 }
147 
149 {
150  // Round up, rather than truncating.
151  return {to_draw_scale(p.x), to_draw_scale(p.y)};
152 }
153 
155 {
156  update_pixel_scale(); // TODO: this should be in recalculate()
157  recalculate();
158 
159  return to_draw_scale({rect_.width, rect_.height});
160 }
161 
163 {
164  recalculate();
165 
166  return (pango_layout_is_ellipsized(layout_.get()) != 0);
167 }
168 
169 unsigned pango_text::insert_text(const unsigned offset, const std::string& text, const bool use_markup)
170 {
171  if (text.empty() || length_ == maximum_length_) {
172  return 0;
173  }
174 
175  // do we really need that assert? utf8::insert will just append in this case, which seems fine
176  assert(offset <= length_);
177 
178  unsigned len = utf8::size(text);
179  if (length_ + len > maximum_length_) {
180  len = maximum_length_ - length_;
181  }
182  const std::string insert = text.substr(0, utf8::index(text, len));
183  std::string tmp = text_;
184  set_text(utf8::insert(tmp, offset, insert), use_markup);
185  // report back how many characters were actually inserted (e.g. to move the cursor selection)
186  return len;
187 }
188 
189 unsigned pango_text::get_byte_index(const unsigned offset, const unsigned line) const
190 {
191  // Determing byte offset
192  std::unique_ptr<PangoLayoutIter, std::function<void(PangoLayoutIter*)>> itor(
193  pango_layout_get_iter(layout_.get()), pango_layout_iter_free);
194 
195  // Go the wanted line.
196  if(line != 0) {
197 
198  if(static_cast<int>(line) >= pango_layout_get_line_count(layout_.get())) {
199  return 0;
200  }
201 
202  for(std::size_t i = 0; i < line; ++i) {
203  pango_layout_iter_next_line(itor.get());
204  }
205  }
206 
207  // Go the wanted column.
208  for(std::size_t i = 0; i < offset; ++i) {
209  if(!pango_layout_iter_next_char(itor.get())) {
210  // It seems that the documentation is wrong and causes and off by
211  // one error... the result should be false if already at the end of
212  // the data when started.
213  if(i + 1 == offset) {
214  break;
215  }
216  // Beyond data.
217  return 0;
218  }
219  }
220 
221  // Get the byte offset
222  return pango_layout_iter_get_index(itor.get());
223 }
224 
225 point pango_text::get_cursor_position(const unsigned offset, const unsigned line) const
226 {
227  recalculate();
229 }
230 
231 point pango_text::get_cursor_pos_from_index(const unsigned offset) const
232 {
233  // Convert the byte offset to a position.
234  PangoRectangle rect;
235  pango_layout_get_cursor_pos(layout_.get(), offset, &rect, nullptr);
236 
237  return to_draw_scale({PANGO_PIXELS(rect.x), PANGO_PIXELS(rect.y)});
238 }
239 
241 {
242  return maximum_length_;
243 }
244 
245 std::string pango_text::get_token(const point& position, const std::string_view delim) const
246 {
247  // Get the index of the character.
248  int index = xy_to_index(position).first;
249  std::string txt = pango_layout_get_text(layout_.get());
250 
251  if (index < 0 || (static_cast<std::size_t>(index) >= txt.size()) || delim.find(txt.at(index)) != std::string::npos) {
252  return ""; // if the index is out of bounds, or the index character is a delimiter, return nothing
253  }
254 
255  std::size_t l = index;
256  while (l > 0 && (delim.find(txt.at(l-1)) == std::string::npos)) {
257  --l;
258  }
259 
260  std::size_t r = index + 1;
261  while (r < txt.size() && (delim.find(txt.at(r)) == std::string::npos)) {
262  ++r;
263  }
264 
265  return txt.substr(l,r-l);
266 }
267 
268 std::string pango_text::get_link(const point& position) const
269 {
270  if (!link_aware_) {
271  return "";
272  }
273 
274  std::string tok = get_token(position);
275  return looks_like_url(tok) ? tok : "";
276 }
277 
279 {
280  // Get the index of the character.
281  const auto [index, trailing] = xy_to_index(position);
282 
283  // Extract the line and the offset in pixels in that line.
284  int line, offset;
285  pango_layout_index_to_line_x(layout_.get(), index, trailing, &line, &offset);
286  offset = PANGO_PIXELS(offset);
287 
288  // Now convert this offset to a column, this way is a bit hacky but haven't
289  // found a better solution yet.
290 
291  /**
292  * @todo There's still a bug left. When you select a text which is in the
293  * ellipses on the right side the text gets reformatted with ellipses on
294  * the left and the selected character is not the one under the cursor.
295  * Other widget toolkits don't show ellipses and have no indication more
296  * text is available. Haven't found what the best thing to do would be.
297  * Until that time leave it as is.
298  */
299  for(std::size_t i = 0; ;++i) {
300  const int pos = get_cursor_position(i, line).x;
301 
302  if(pos == offset) {
303  // FIXME: return statement only inside if block.
304  return point(i, line);
305  }
306  }
307 }
308 
309 std::pair<int, int> pango_text::xy_to_index(const point& position) const
310 {
311  recalculate();
312 
313  // Get the index of the character.
314  int index, trailing;
315  if(!pango_layout_xy_to_index(layout_.get(), position.x * PANGO_SCALE, position.y * PANGO_SCALE, &index, &trailing))
316  {
317  index = -1;
318  trailing = -1;
319  }
320 
321  return { index, trailing };
322 }
323 
325 {
326  pango_layout_set_attributes(layout_.get(), nullptr);
327 }
328 
330 {
331  if(PangoAttrList* current_attrs = pango_layout_get_attributes(layout_.get())) {
332  attrs.splice_into(current_attrs);
333  } else {
334  attrs.apply_to(layout_.get());
335  }
336 }
337 
338 bool pango_text::set_text(const std::string& text, const bool markedup)
339 {
340  if(markedup != markedup_text_ || text != text_) {
341  const std::u32string wide = unicode_cast<std::u32string>(text);
342  std::string narrow = unicode_cast<std::string>(wide);
343  if(text != narrow) {
344  ERR_GUI_L
345  << "pango_text::" << __func__
346  << " text '" << text
347  << "' contains invalid utf-8, trimmed the invalid parts.";
348  }
349 
350  if(!markedup || !set_markup(narrow, *layout_)) {
351  pango_layout_set_text(layout_.get(), narrow.c_str(), narrow.size());
353  }
354 
355  text_ = std::move(narrow);
356  length_ = wide.size();
357  markedup_text_ = markedup;
358  calculation_dirty_ = true;
359  }
360 
361  return true;
362 }
363 
365 {
366  if(fclass != font_class_) {
367  font_class_ = fclass;
368  calculation_dirty_ = true;
369  }
370 
371  return *this;
372 }
373 
375 {
376  font_size = prefs::get().font_scaled(font_size) * pixel_scale_;
377 
378  if(font_size != font_size_) {
379  font_size_ = font_size;
380  calculation_dirty_ = true;
381  }
382 
383  return *this;
384 }
385 
387 {
388  if(font_style != font_style_) {
389  font_style_ = font_style;
390  calculation_dirty_ = true;
391  }
392 
393  return *this;
394 }
395 
397 {
398  if(color != foreground_color_) {
399  foreground_color_ = color;
400  }
401 
402  return *this;
403 }
404 
406 {
407  width *= pixel_scale_;
408 
409  if(width <= 0) {
410  width = -1;
411  }
412 
413  if(width != maximum_width_) {
414  maximum_width_ = width;
415  calculation_dirty_ = true;
416  }
417 
418  return *this;
419 }
420 
421 pango_text& pango_text::set_characters_per_line(const unsigned characters_per_line)
422 {
423  if(characters_per_line != characters_per_line_) {
424  characters_per_line_ = characters_per_line;
425 
426  calculation_dirty_ = true;
427  }
428 
429  return *this;
430 }
431 
432 pango_text& pango_text::set_maximum_height(int height, bool multiline)
433 {
434  height *= pixel_scale_;
435 
436  if(height <= 0) {
437  height = -1;
438  multiline = false;
439  }
440 
441  if(height != maximum_height_) {
442  // assert(context_);
443 
444  // The maximum height is handled in this class' calculate_size() method.
445  //
446  // Although we also pass it to PangoLayout if multiline is true, the documentation of pango_layout_set_height
447  // makes me wonder whether we should avoid that function completely. For example, "at least one line is included
448  // in each paragraph regardless" and "may be changed in future, file a bug if you rely on the current behavior".
449  pango_layout_set_height(layout_.get(), !multiline ? -1 : height * PANGO_SCALE);
450  maximum_height_ = height;
451  calculation_dirty_ = true;
452  }
453 
454  return *this;
455 }
456 
457 pango_text& pango_text::set_ellipse_mode(const PangoEllipsizeMode ellipse_mode)
458 {
459  if(ellipse_mode != ellipse_mode_) {
460  // assert(context_);
461 
462  pango_layout_set_ellipsize(layout_.get(), ellipse_mode);
463  ellipse_mode_ = ellipse_mode;
464  calculation_dirty_ = true;
465  }
466 
467  // According to the docs of pango_layout_set_height, the behavior is undefined if a height other than -1 is combined
468  // with PANGO_ELLIPSIZE_NONE. Wesnoth's code currently always calls set_ellipse_mode after set_maximum_height, so do
469  // the cleanup here. The code in calculate_size() will still apply the maximum height after Pango's calculations.
470  if(ellipse_mode_ == PANGO_ELLIPSIZE_NONE) {
471  pango_layout_set_height(layout_.get(), -1);
472  }
473 
474  return *this;
475 }
476 
477 pango_text &pango_text::set_alignment(const PangoAlignment alignment)
478 {
479  if (alignment != alignment_) {
480  pango_layout_set_alignment(layout_.get(), alignment);
481  alignment_ = alignment;
482  }
483 
484  return *this;
485 }
486 
487 pango_text& pango_text::set_maximum_length(const std::size_t maximum_length)
488 {
489  if(maximum_length != maximum_length_) {
490  maximum_length_ = maximum_length;
491  if(length_ > maximum_length_) {
492  std::string tmp = text_;
494  }
495  }
496 
497  return *this;
498 }
499 
501 {
502  if (link_aware_ != b) {
503  calculation_dirty_ = true;
504  link_aware_ = b;
505  }
506  return *this;
507 }
508 
510 {
511  if(color != link_color_) {
512  link_color_ = color;
513  calculation_dirty_ = true;
514  }
515 
516  return *this;
517 }
518 
520 {
521  if(do_add != add_outline_) {
522  add_outline_ = do_add;
523  //calculation_dirty_ = true;
524  }
525 
526  return *this;
527 }
528 
530 {
532 
533  PangoFont* f = pango_font_map_load_font(
534  pango_cairo_font_map_get_default(),
535  context_.get(),
536  font.get());
537 
538  PangoFontMetrics* m = pango_font_get_metrics(f, nullptr);
539 
540  auto ascent = pango_font_metrics_get_ascent(m);
541  auto descent = pango_font_metrics_get_descent(m);
542 
543  pango_font_metrics_unref(m);
544  g_object_unref(f);
545 
546  return ceil(pango_units_to_double(ascent + descent) / pixel_scale_);
547 }
548 
550 {
551  const int ps = video::get_pixel_scale();
552  if (ps == pixel_scale_) {
553  return;
554  }
555 
557 
558  if (maximum_width_ != -1) {
560  }
561 
562  if (maximum_height_ != -1) {
564  }
565 
566  calculation_dirty_ = true;
567  pixel_scale_ = ps;
568 }
569 
571 {
572  // TODO: clean up this "const everything then mutable everything" mess.
573  // update_pixel_scale() should go in here. But it can't. Because things
574  // are declared const which are not const.
575 
576  if(calculation_dirty_) {
577  assert(layout_ != nullptr);
578 
579  calculation_dirty_ = false;
581  }
582 }
583 
584 PangoRectangle pango_text::calculate_size(PangoLayout& layout) const
585 {
586  PangoRectangle size;
587 
589  pango_layout_set_font_description(&layout, font.get());
590 
591  int maximum_width = 0;
592  if(characters_per_line_ != 0) {
593  PangoFont* f = pango_font_map_load_font(
594  pango_cairo_font_map_get_default(),
595  context_.get(),
596  font.get());
597 
598  PangoFontMetrics* m = pango_font_get_metrics(f, nullptr);
599 
600  int w = pango_font_metrics_get_approximate_char_width(m);
602 
603  maximum_width = ceil(pango_units_to_double(w));
604 
605  pango_font_metrics_unref(m);
606  g_object_unref(f);
607  } else {
608  maximum_width = maximum_width_;
609  }
610 
611  if(maximum_width_ != -1) {
612  maximum_width = std::min(maximum_width, maximum_width_);
613  }
614 
615  pango_layout_set_width(&layout, maximum_width == -1
616  ? -1
617  : maximum_width * PANGO_SCALE);
618  pango_layout_get_pixel_extents(&layout, nullptr, &size);
619 
620  DBG_GUI_L << "pango_text::" << __func__
621  << " text '" << gui2::debug_truncate(text_)
622  << "' maximum_width " << maximum_width
623  << " width " << size.x + size.width
624  << ".";
625 
626  DBG_GUI_L << "pango_text::" << __func__
627  << " text '" << gui2::debug_truncate(text_)
628  << "' font_size " << font_size_
629  << " markedup_text " << markedup_text_
630  << " font_style " << std::hex << font_style_ << std::dec
631  << " maximum_width " << maximum_width
632  << " maximum_height " << maximum_height_
633  << " result " << size
634  << ".";
635 
636  if(maximum_width != -1 && size.x + size.width > maximum_width) {
637  DBG_GUI_L << "pango_text::" << __func__
638  << " text '" << gui2::debug_truncate(text_)
639  << " ' width " << size.x + size.width
640  << " greater as the wanted maximum of " << maximum_width
641  << ".";
642  }
643 
644  // The maximum height is handled here instead of using the library - see the comments in set_maximum_height()
645  if(maximum_height_ != -1 && size.y + size.height > maximum_height_) {
646  DBG_GUI_L << "pango_text::" << __func__
647  << " text '" << gui2::debug_truncate(text_)
648  << " ' height " << size.y + size.height
649  << " greater as the wanted maximum of " << maximum_height_
650  << ".";
651  size.height = maximum_height_ - std::max(0, size.y);
652  }
653 
654  return size;
655 }
656 
657 /***
658  * Inverse table
659  *
660  * Holds a high-precision inverse for each number i, that is, a number x such that x * i / 256 is close to 255.
661  */
663 {
664  unsigned values[256] {};
665 
666  constexpr inverse_table()
667  {
668  values[0] = 0;
669  for (int i = 1; i < 256; ++i) {
670  values[i] = (255 * 256) / i;
671  }
672  }
673 
674  unsigned operator[](uint8_t i) const { return values[i]; }
675 };
676 
677 static constexpr inverse_table inverse_table_;
678 
679 /***
680  * Helper function for un-premultiplying alpha
681  * Div should be the high-precision inverse for the alpha value.
682  */
683 static void unpremultiply(uint8_t & value, const unsigned div) {
684  unsigned temp = (value * div) / 256u;
685  // Note: It's always the case that alpha * div < 256 if div is the inverse
686  // for alpha, so if cairo is computing premultiplied alpha by rounding down,
687  // this min is not necessary. However, if cairo generates illegal output,
688  // the min may be selected.
689  // It's probably not worth removing the min, since branch prediction will
690  // make it essentially free if one of the branches is never actually
691  // selected.
692  value = std::min(255u, temp);
693 }
694 
695 /**
696  * Converts from cairo-format ARGB32 premultiplied alpha to plain alpha.
697  * @param c a uint32 representing the color
698  */
699 static void from_cairo_format(uint32_t & c)
700 {
701  uint8_t a = (c >> 24) & 0xff;
702  uint8_t r = (c >> 16) & 0xff;
703  uint8_t g = (c >> 8) & 0xff;
704  uint8_t b = c & 0xff;
705 
706  const unsigned div = inverse_table_[a];
707  unpremultiply(r, div);
708  unpremultiply(g, div);
709  unpremultiply(b, div);
710 
711  c = (static_cast<uint32_t>(a) << 24) | (static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | static_cast<uint32_t>(b);
712 }
713 
714 void pango_text::render(PangoLayout& layout, const SDL_Rect& viewport)
715 {
716  auto cairo_surface = cairo::create_surface(&surface_buffer_[0], point{ viewport.w, viewport.h }); // TODO: use rect::size
717  auto cairo_context = cairo::create_context(cairo_surface);
718 
719  // Convenience pointer
720  cairo_t* cr = cairo_context.get();
721 
722  if(cairo_status(cr) == CAIRO_STATUS_INVALID_SIZE) {
723  throw std::length_error("Text is too long to render");
724  }
725 
726  // The top-left of the text, which can be outside the area to be rendered
727  cairo_move_to(cr, -viewport.x, -viewport.y);
728 
729  //
730  // TODO: the outline may be slightly cut off around certain text if it renders too
731  // close to the surface's edge. That causes the outline to extend just slightly
732  // outside the surface's borders. I'm not sure how best to deal with this. Obviously,
733  // we want to increase the surface size, but we also don't want to invalidate all
734  // the placement and size calculations. Thankfully, it's not very noticeable.
735  //
736  // -- vultraz, 2018-03-07
737  //
738  if(add_outline_) {
739  // Add a path to the cairo context tracing the current text.
740  pango_cairo_layout_path(cr, &layout);
741 
742  // Set color for background outline (black).
743  cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 1.0);
744 
745  cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
746  cairo_set_line_width(cr, 3.0); // Adjust as necessary
747 
748  // Stroke path to draw outline.
749  cairo_stroke(cr);
750  }
751 
752  // Set main text color.
753  cairo_set_source_rgba(cr,
754  foreground_color_.r / 255.0,
755  foreground_color_.g / 255.0,
756  foreground_color_.b / 255.0,
757  foreground_color_.a / 255.0
758  );
759 
762  list.insert(pango_attr_underline_new(PANGO_UNDERLINE_SINGLE));
763  apply_attributes(list);
764  }
765 
766  pango_cairo_show_layout(cr, &layout);
767 }
768 
770 {
771  return create_surface({0, 0, rect_.x + rect_.width, rect_.y + rect_.height});
772 }
773 
774 surface pango_text::create_surface(const SDL_Rect& viewport)
775 {
776  assert(layout_.get());
777 
778  cairo_format_t format = CAIRO_FORMAT_ARGB32;
779  const int stride = cairo_format_stride_for_width(format, viewport.w);
780 
781  // The width and stride can be zero if the text is empty or the stride can be negative to indicate an error from
782  // Cairo. Width isn't tested here because it's implied by stride.
783  if(stride <= 0 || viewport.h <= 0) {
784  surface_buffer_.clear();
785  return nullptr;
786  }
787 
788  DBG_FT << "creating new text surface";
789 
790  // Check to prevent arithmetic overflow when calculating (stride * height).
791  // The size of the viewport should already provide a far lower limit on the
792  // maximum size, but this is left in as a sanity check.
793  if(viewport.h > std::numeric_limits<int>::max() / stride) {
794  throw std::length_error("Text is too long to render");
795  }
796 
797  // Resize buffer appropriately and set all pixel values to 0.
798  surface_buffer_.assign(viewport.h * stride, 0);
799 
800  // Try rendering the whole text in one go. If this throws a length_error
801  // then leave it to the caller to handle; one reason it may throw is that
802  // cairo surfaces are limited to approximately 2**15 pixels in height.
803  render(*layout_, viewport);
804 
805  // The cairo surface is in CAIRO_FORMAT_ARGB32 which uses
806  // pre-multiplied alpha. SDL doesn't use that so the pixels need to be
807  // decoded again.
808  for(int y = 0; y < viewport.h; ++y) {
809  uint32_t* pixels = reinterpret_cast<uint32_t*>(&surface_buffer_[y * stride]);
810  for(int x = 0; x < viewport.w; ++x) {
811  from_cairo_format(pixels[x]);
812  }
813  }
814 
815  return SDL_CreateRGBSurfaceWithFormatFrom(
816  &surface_buffer_[0], viewport.w, viewport.h, 32, stride, SDL_PIXELFORMAT_ARGB8888);
817 }
818 
819 bool pango_text::set_markup(std::string_view text, PangoLayout& layout)
820 {
821  char* raw_text;
822  std::string semi_escaped;
823  bool valid = validate_markup(text, &raw_text, semi_escaped);
824  if(!semi_escaped.empty()) {
825  text = semi_escaped;
826  }
827 
828  if(valid) {
829  if(link_aware_) {
830  std::string formatted_text = format_links(text);
831  pango_layout_set_markup(&layout, formatted_text.c_str(), formatted_text.size());
832  } else {
833  pango_layout_set_markup(&layout, text.data(), text.size());
834  }
835  }
836 
837  return valid;
838 }
839 
840 /**
841  * Replaces all instances of URLs in a given string with formatted links
842  * and returns the result.
843  */
844 std::string pango_text::format_links(std::string_view text) const
845 {
846  static const std::string delim = " \n\r\t";
847  std::ostringstream result;
848 
849  std::size_t tok_start = 0;
850  for(std::size_t pos = 0; pos < text.length(); ++pos) {
851  if(delim.find(text[pos]) == std::string::npos) {
852  continue;
853  }
854 
855  if(const auto tok_length = pos - tok_start) {
856  // Token starts from after the last delimiter up to (but not including) this delimiter
857  auto token = text.substr(tok_start, tok_length);
858  if(looks_like_url(token)) {
859  result << format_as_link(std::string{token}, link_color_);
860  } else {
861  result << token;
862  }
863  }
864 
865  result << text[pos];
866  tok_start = pos + 1;
867  }
868 
869  // Deal with the remainder token
870  if(tok_start < text.length()) {
871  auto token = text.substr(tok_start);
872  if(looks_like_url(token)) {
873  result << format_as_link(std::string{token}, link_color_);
874  } else {
875  result << token;
876  }
877  }
878 
879  return result.str();
880 }
881 
882 bool pango_text::validate_markup(std::string_view text, char** raw_text, std::string& semi_escaped) const
883 {
884  if(pango_parse_markup(text.data(), text.size(),
885  0, nullptr, raw_text, nullptr, nullptr)) {
886  return true;
887  }
888 
889  /*
890  * The markup is invalid. Try to recover.
891  *
892  * The pango engine tested seems to accept stray single quotes »'« and
893  * double quotes »"«. Stray ampersands »&« seem to give troubles.
894  * So only try to recover from broken ampersands, by simply replacing them
895  * with the escaped version.
896  */
897  semi_escaped = semi_escape_text(text);
898 
899  /*
900  * If at least one ampersand is replaced the semi-escaped string
901  * is longer than the original. If this isn't the case then the
902  * markup wasn't (only) broken by ampersands in the first place.
903  */
904  if(text.size() == semi_escaped.size()
905  || !pango_parse_markup(semi_escaped.c_str(), semi_escaped.size()
906  , 0, nullptr, raw_text, nullptr, nullptr)) {
907 
908  /* Fixing the ampersands didn't work. */
909  return false;
910  }
911 
912  /* Replacement worked, still warn the user about the error. */
913  WRN_GUI_L << "pango_text::" << __func__
914  << " text '" << text
915  << "' has unescaped ampersands '&', escaped them.";
916 
917  return true;
918 }
919 
920 void pango_text::copy_layout_properties(PangoLayout& src, PangoLayout& dst)
921 {
922  pango_layout_set_alignment(&dst, pango_layout_get_alignment(&src));
923  pango_layout_set_height(&dst, pango_layout_get_height(&src));
924  pango_layout_set_ellipsize(&dst, pango_layout_get_ellipsize(&src));
925 }
926 
927 std::vector<std::string> pango_text::get_lines() const
928 {
929  recalculate();
930 
931  PangoLayout* const layout = layout_.get();
932  std::vector<std::string> res;
933  int count = pango_layout_get_line_count(layout);
934 
935  if(count < 1) {
936  return res;
937  }
938 
939  using layout_iterator = std::unique_ptr<PangoLayoutIter, std::function<void(PangoLayoutIter*)>>;
940  layout_iterator i{pango_layout_get_iter(layout), pango_layout_iter_free};
941 
942  res.reserve(count);
943 
944  do {
945  PangoLayoutLine* ll = pango_layout_iter_get_line_readonly(i.get());
946  const char* begin = &pango_layout_get_text(layout)[ll->start_index];
947  res.emplace_back(begin, ll->length);
948  } while(pango_layout_iter_next_line(i.get()));
949 
950  return res;
951 }
952 
953 PangoLayoutLine* pango_text::get_line(int index)
954 {
955  return pango_layout_get_line_readonly(layout_.get(), index);
956 }
957 
958 int pango_text::get_line_num_from_offset(const unsigned offset)
959 {
960  int line_num = 0;
961  pango_layout_index_to_line_x(layout_.get(), offset, 0, &line_num, nullptr);
962  return line_num;
963 }
964 
966 {
967  static pango_text text_renderer;
968  return text_renderer;
969 }
970 
972 {
973  // Reset metrics to defaults
974  return get_text_renderer()
975  .set_family_class(fclass)
976  .set_font_style(style)
979 }
980 
981 } // namespace font
double t
Definition: astarsearch.cpp:63
double g
Definition: astarsearch.cpp:63
Helper class to encapsulate the management of a PangoAttrList.
Definition: attributes.hpp:28
void splice_into(PangoAttrList *target) const
Definition: attributes.hpp:71
void apply_to(PangoLayout *layout) const
Definition: attributes.hpp:66
void insert(PangoAttribute *attr)
Definition: attributes.hpp:56
Small helper class to make sure the pango font object is destroyed properly.
Definition: font.hpp:25
Text class.
Definition: text.hpp:78
int pixel_scale_
The pixel scale, used to render high-DPI text.
Definition: text.hpp:442
pango_text & set_font_style(const FONT_STYLE font_style)
Definition: text.cpp:386
PangoEllipsizeMode ellipse_mode_
The way too long text is shown depends on this mode.
Definition: text.hpp:421
static void copy_layout_properties(PangoLayout &src, PangoLayout &dst)
Definition: text.cpp:920
bool add_outline_
Whether to add an outline effect.
Definition: text.hpp:382
void clear_attributes()
Definition: text.cpp:324
bool validate_markup(std::string_view text, char **raw_text, std::string &semi_escaped) const
Definition: text.cpp:882
bool set_markup(std::string_view text, PangoLayout &layout)
Sets the markup'ed text.
Definition: text.cpp:819
pango_text & set_maximum_length(const std::size_t maximum_length)
Definition: text.cpp:487
surface create_surface()
Equivalent to create_surface(viewport), where the viewport's top-left is at (0,0) and the area is lar...
Definition: text.cpp:769
PangoAlignment alignment_
The alignment of the text.
Definition: text.hpp:424
PangoRectangle rect_
Definition: text.hpp:349
int maximum_height_
The maximum height of the text.
Definition: text.hpp:418
point get_size()
Returns the size of the text, in drawing coordinates.
Definition: text.cpp:154
pango_text & set_characters_per_line(const unsigned characters_per_line)
Definition: text.cpp:421
color_t link_color_
The color to render links in.
Definition: text.hpp:367
unsigned insert_text(const unsigned offset, const std::string &text, const bool use_markup=false)
Inserts UTF-8 text.
Definition: text.cpp:169
int get_line_num_from_offset(const unsigned offset)
Given a byte index, find out at which line the corresponding character is located.
Definition: text.cpp:958
void recalculate() const
Recalculates the text layout.
Definition: text.cpp:570
void render(PangoLayout &layout, const SDL_Rect &viewport)
This is part of create_surface(viewport).
Definition: text.cpp:714
color_t foreground_color_
The foreground color.
Definition: text.hpp:379
point get_column_line(const point &position) const
Gets the column of line of the character at the position.
Definition: text.cpp:278
std::unique_ptr< PangoContext, std::function< void(void *)> > context_
Definition: text.hpp:347
bool link_aware_
Are hyperlinks in the text marked-up, and will get_link return them.
Definition: text.hpp:358
pango_text & set_foreground_color(const color_t &color)
Definition: text.cpp:396
int to_draw_scale(int s) const
Scale the given render-space size to draw-space, rounding up.
Definition: text.cpp:143
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:844
unsigned get_byte_index(const unsigned offset, const unsigned line=0) const
Given a character index and optionally the starting line, returns the corresponding byte index.
Definition: text.cpp:189
std::string get_token(const point &position, std::string_view delimiters=" \n\r\t") const
Gets the largest collection of characters, including the token at position, and not including any cha...
Definition: text.cpp:245
PangoLayoutLine * get_line(int index)
Get a specific line from the pango layout.
Definition: text.cpp:953
unsigned characters_per_line_
The number of characters per line.
Definition: text.hpp:410
bool markedup_text_
Does the text contain pango markup? If different render routines must be used.
Definition: text.hpp:355
pango_text & set_family_class(font::family_class fclass)
Definition: text.cpp:364
font::family_class font_class_
The font family class used.
Definition: text.hpp:370
void apply_attributes(const font::attribute_list &attrs)
Definition: text.cpp:329
std::pair< int, int > xy_to_index(const point &position) const
Definition: text.cpp:309
std::vector< std::string > get_lines() const
Retrieves a list of strings with contents for each rendered line.
Definition: text.cpp:927
void update_pixel_scale()
Update pixel scale, if necessary.
Definition: text.cpp:549
unsigned font_size_
The font size to draw.
Definition: text.hpp:373
texture render_texture(const SDL_Rect &viewport)
Wrapper around render_surface which sets texture::w() and texture::h() in the same way that render_an...
Definition: text.cpp:117
surface render_surface(const SDL_Rect &viewport)
Returns the rendered text.
Definition: text.cpp:129
std::vector< uint8_t > surface_buffer_
Buffer to store the image on.
Definition: text.hpp:487
pango_text & set_add_outline(bool do_add)
Definition: text.cpp:519
point get_cursor_position(const unsigned offset, const unsigned line=0) const
Gets the location for the cursor, in drawing coordinates.
Definition: text.cpp:225
pango_text & set_ellipse_mode(const PangoEllipsizeMode ellipse_mode)
Definition: text.cpp:457
PangoRectangle calculate_size(PangoLayout &layout) const
Calculates surface size.
Definition: text.cpp:584
pango_text & set_alignment(const PangoAlignment alignment)
Definition: text.cpp:477
std::string text_
The text to draw (stored as UTF-8).
Definition: text.hpp:352
bool calculation_dirty_
The text has two dirty states:
Definition: text.hpp:436
std::unique_ptr< PangoLayout, std::function< void(void *)> > layout_
Definition: text.hpp:348
pango_text & set_font_size(unsigned font_size)
Definition: text.cpp:374
pango_text & set_link_aware(bool b)
Definition: text.cpp:500
FONT_STYLE font_style_
The style of the font, this is an orred mask of the font flags.
Definition: text.hpp:376
bool set_text(const std::string &text, const bool markedup)
Sets the text to render.
Definition: text.cpp:338
bool is_truncated() const
Has the text been truncated? This happens if it exceeds max width or height.
Definition: text.cpp:162
texture with_draw_scale(const texture &t) const
Adjust a texture's draw-width and height according to pixel scale.
Definition: text.cpp:136
std::size_t maximum_length_
The maximum length of the text.
Definition: text.hpp:427
point get_cursor_pos_from_index(const unsigned offset) const
Gets the location for the cursor, in drawing coordinates.
Definition: text.cpp:231
pango_text & set_maximum_height(int height, bool multiline)
Definition: text.cpp:432
pango_text & set_maximum_width(int width)
Definition: text.cpp:405
std::size_t get_maximum_length() const
Get maximum length.
Definition: text.cpp:240
texture render_and_get_texture()
Returns the cached texture, or creates a new one otherwise.
Definition: text.cpp:122
pango_text & set_link_color(const color_t &color)
Definition: text.cpp:509
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:268
const std::string & text() const
Definition: text.hpp:305
std::size_t length_
Length of the text.
Definition: text.hpp:439
int get_max_glyph_height() const
Returns the maximum glyph height of a font, in drawing coordinates.
Definition: text.cpp:529
int maximum_width_
The maximum width of the text.
Definition: text.hpp:392
static prefs & get()
int font_scaled(int size)
Wrapper class to encapsulate creation and management of an SDL_Texture.
Definition: texture.hpp:33
void set_draw_size(int w, int h)
Set the intended size of the texture, in draw-space.
Definition: texture.hpp:129
std::size_t i
Definition: function.cpp:1030
int w
Define the common log macros for the gui toolkit.
#define DBG_GUI_L
Definition: log.hpp:55
#define ERR_GUI_L
Definition: log.hpp:58
#define WRN_GUI_L
Definition: log.hpp:57
context_ptr create_context(const surface_ptr &surf)
Definition: cairo.hpp:41
surface_ptr create_surface(uint8_t *buffer, const point &size)
Definition: cairo.hpp:30
CURSOR_TYPE get()
Definition: cursor.cpp:218
static void layout()
void point(int x, int y)
Draw a single point.
Definition: draw.cpp:211
void rect(const SDL_Rect &rect)
Draw a rectangle.
Definition: draw.cpp:159
void line(int from_x, int from_y, int to_x, int to_y)
Draw a line.
Definition: draw.cpp:189
Graphical text output.
pango_text & get_text_renderer()
Returns a reference to a static pango_text object.
Definition: text.cpp:965
constexpr float get_line_spacing_factor()
Definition: text.hpp:553
bool looks_like_url(std::string_view str)
Definition: hyperlink.hpp:28
const t_string & get_font_families(family_class fclass)
Returns the currently defined fonts.
Definition: font_config.cpp:93
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:971
std::string semi_escape_text(std::string_view text)
Definition: escape.hpp:52
std::string format_as_link(const std::string &link, color_t color)
Definition: hyperlink.hpp:33
static void unpremultiply(uint8_t &value, const unsigned div)
Definition: text.cpp:683
static void from_cairo_format(uint32_t &c)
Converts from cairo-format ARGB32 premultiplied alpha to plain alpha.
Definition: text.cpp:699
static constexpr inverse_table inverse_table_
Definition: text.cpp:677
std::string_view debug_truncate(std::string_view text)
Returns a truncated version of the text.
Definition: helper.cpp:173
std::string img(const std::string &src, const std::string &align, bool floating)
Generates a Help markup tag corresponding to an image.
Definition: markup.cpp:31
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:98
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:85
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:116
std::size_t index(std::string_view str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:70
int get_pixel_scale()
Get the current active pixel scale multiplier.
Definition: video.cpp:481
rect dst
Location on the final composed sheet.
rect src
Non-transparent portion of the surface to compose.
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:61
unsigned values[256]
Definition: text.cpp:664
constexpr inverse_table()
Definition: text.cpp:666
unsigned operator[](uint8_t i) const
Definition: text.cpp:674
Holds a 2D point.
Definition: point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:49
contains the current text being parsed as well as the token_type of what's being parsed.
Definition: tokenizer.hpp:41
mock_char c
mock_party p
#define DBG_FT
Definition: text.cpp:42
static lg::log_domain log_font("font")
#define f
#define b