The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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"
28
29#include "gettext.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
41static lg::log_domain log_font("font");
42#define DBG_FT LOG_STREAM(debug, log_font)
43
44namespace font
45{
46
47namespace
48{
49void 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);
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
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
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
169unsigned 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
189unsigned 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
225point pango_text::get_cursor_position(const unsigned offset, const unsigned line) const
226{
227 recalculate();
228 return get_cursor_pos_from_index(get_byte_index(offset, line));
229}
230
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
245std::string pango_text::get_token(const point& position, const std::string_view delim) const
246{
247 // Get the index of the character.
248 const auto [index, _, inside_bounds] = xy_to_index(position);
249 std::string txt = pango_layout_get_text(layout_.get());
250
251 if (!inside_bounds || 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
268std::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 auto [line, offset] = index_to_line_x(index, trailing);
285 offset = PANGO_PIXELS(offset);
286
287 // Now convert this offset to a column, this way is a bit hacky but haven't
288 // found a better solution yet.
289
290 /**
291 * @todo There's still a bug left. When you select a text which is in the
292 * ellipses on the right side the text gets reformatted with ellipses on
293 * the left and the selected character is not the one under the cursor.
294 * Other widget toolkits don't show ellipses and have no indication more
295 * text is available. Haven't found what the best thing to do would be.
296 * Until that time leave it as is.
297 */
298 for(std::size_t i = 0; ;++i) {
299 const int pos = get_cursor_position(i, line).x;
300
301 if(pos == offset) {
302 // FIXME: return statement only inside if block.
303 return point(i, line);
304 }
305 }
306}
307
308std::tuple<int, int, bool> pango_text::xy_to_index(const point& position) const
309{
310 recalculate();
311
312 // Get the index of the character.
313 int index, trailing;
314 int res = pango_layout_xy_to_index(layout_.get(), position.x * PANGO_SCALE, position.y * PANGO_SCALE, &index, &trailing);
315 // res is gboolean
316 return { index, trailing, res != 0 };
317}
318
320{
321 pango_layout_set_attributes(layout_.get(), nullptr);
322}
323
325{
326 if(PangoAttrList* current_attrs = pango_layout_get_attributes(layout_.get())) {
327 attrs.splice_into(current_attrs);
328 } else {
329 attrs.apply_to(layout_.get());
330 }
331}
332
333bool pango_text::set_text(const std::string& text, const bool markedup)
334{
335 if(markedup != markedup_text_ || text != text_) {
336 const std::u32string wide = unicode_cast<std::u32string>(text);
337 std::string narrow = unicode_cast<std::string>(wide);
338 if(text != narrow) {
340 << "pango_text::" << __func__
341 << " text '" << text
342 << "' contains invalid utf-8, trimmed the invalid parts.";
343 }
344
345 if(!markedup || !set_markup(narrow, *layout_)) {
346 pango_layout_set_text(layout_.get(), narrow.c_str(), narrow.size());
348 }
349
350 text_ = std::move(narrow);
351 length_ = wide.size();
352 markedup_text_ = markedup;
353 calculation_dirty_ = true;
354 }
355
356 return true;
357}
358
360{
361 if(fclass != font_class_) {
362 font_class_ = fclass;
363 calculation_dirty_ = true;
364 }
365
366 return *this;
367}
368
370{
371 font_size = prefs::get().font_scaled(font_size) * pixel_scale_;
372
373 if(font_size != font_size_) {
374 font_size_ = font_size;
375 calculation_dirty_ = true;
376 }
377
378 return *this;
379}
380
382{
383 if(font_style != font_style_) {
384 font_style_ = font_style;
385 calculation_dirty_ = true;
386 }
387
388 return *this;
389}
390
392{
393 if(color != foreground_color_) {
394 foreground_color_ = color;
395 }
396
397 return *this;
398}
399
401{
402 width *= pixel_scale_;
403
404 if(width <= 0) {
405 width = -1;
406 }
407
408 if(width != maximum_width_) {
409 maximum_width_ = width;
410 calculation_dirty_ = true;
411 }
412
413 return *this;
414}
415
416pango_text& pango_text::set_characters_per_line(const unsigned characters_per_line)
417{
418 if(characters_per_line != characters_per_line_) {
419 characters_per_line_ = characters_per_line;
420
421 calculation_dirty_ = true;
422 }
423
424 return *this;
425}
426
427pango_text& pango_text::set_maximum_height(int height, bool multiline)
428{
429 height *= pixel_scale_;
430
431 if(height <= 0) {
432 height = -1;
433 multiline = false;
434 }
435
436 if(height != maximum_height_) {
437 // assert(context_);
438
439 // The maximum height is handled in this class' calculate_size() method.
440 //
441 // Although we also pass it to PangoLayout if multiline is true, the documentation of pango_layout_set_height
442 // makes me wonder whether we should avoid that function completely. For example, "at least one line is included
443 // in each paragraph regardless" and "may be changed in future, file a bug if you rely on the current behavior".
444 pango_layout_set_height(layout_.get(), !multiline ? -1 : height * PANGO_SCALE);
445 maximum_height_ = height;
446 calculation_dirty_ = true;
447 }
448
449 return *this;
450}
451
452pango_text& pango_text::set_ellipse_mode(const PangoEllipsizeMode ellipse_mode)
453{
454 if(ellipse_mode != ellipse_mode_) {
455 // assert(context_);
456
457 pango_layout_set_ellipsize(layout_.get(), ellipse_mode);
458 ellipse_mode_ = ellipse_mode;
459 calculation_dirty_ = true;
460 }
461
462 // According to the docs of pango_layout_set_height, the behavior is undefined if a height other than -1 is combined
463 // with PANGO_ELLIPSIZE_NONE. Wesnoth's code currently always calls set_ellipse_mode after set_maximum_height, so do
464 // the cleanup here. The code in calculate_size() will still apply the maximum height after Pango's calculations.
465 if(ellipse_mode_ == PANGO_ELLIPSIZE_NONE) {
466 pango_layout_set_height(layout_.get(), -1);
467 }
468
469 return *this;
470}
471
472pango_text &pango_text::set_alignment(const PangoAlignment alignment)
473{
474 if (alignment != alignment_) {
475 pango_layout_set_alignment(layout_.get(), alignment);
476 alignment_ = alignment;
477 }
478
479 return *this;
480}
481
482pango_text& pango_text::set_maximum_length(const std::size_t maximum_length)
483{
484 if(maximum_length != maximum_length_) {
485 maximum_length_ = maximum_length;
487 std::string tmp = text_;
489 }
490 }
491
492 return *this;
493}
494
496{
497 if (link_aware_ != b) {
498 calculation_dirty_ = true;
499 link_aware_ = b;
500 }
501 return *this;
502}
503
505{
506 if(color != link_color_) {
507 link_color_ = color;
508 calculation_dirty_ = true;
509 }
510
511 return *this;
512}
513
515{
516 if(do_add != add_outline_) {
517 add_outline_ = do_add;
518 //calculation_dirty_ = true;
519 }
520
521 return *this;
522}
523
525{
527
528 PangoFont* f = pango_font_map_load_font(
529 pango_cairo_font_map_get_default(),
530 context_.get(),
531 font.get());
532
533 PangoFontMetrics* m = pango_font_get_metrics(f, nullptr);
534
535 auto ascent = pango_font_metrics_get_ascent(m);
536 auto descent = pango_font_metrics_get_descent(m);
537
538 pango_font_metrics_unref(m);
539 g_object_unref(f);
540
541 return ceil(pango_units_to_double(ascent + descent) / pixel_scale_);
542}
543
545{
546 const int ps = video::get_pixel_scale();
547 if (ps == pixel_scale_) {
548 return;
549 }
550
552
553 if (maximum_width_ != -1) {
555 }
556
557 if (maximum_height_ != -1) {
559 }
560
561 calculation_dirty_ = true;
562 pixel_scale_ = ps;
563}
564
566{
567 // TODO: clean up this "const everything then mutable everything" mess.
568 // update_pixel_scale() should go in here. But it can't. Because things
569 // are declared const which are not const.
570
572 assert(layout_ != nullptr);
573
574 calculation_dirty_ = false;
576 }
577}
578
579PangoRectangle pango_text::calculate_size(PangoLayout& layout) const
580{
581 PangoRectangle size;
582
584 pango_layout_set_font_description(&layout, font.get());
585
586 int maximum_width = 0;
587 if(characters_per_line_ != 0) {
588 PangoFont* f = pango_font_map_load_font(
589 pango_cairo_font_map_get_default(),
590 context_.get(),
591 font.get());
592
593 PangoFontMetrics* m = pango_font_get_metrics(f, nullptr);
594
595 int w = pango_font_metrics_get_approximate_char_width(m);
597
598 maximum_width = ceil(pango_units_to_double(w));
599
600 pango_font_metrics_unref(m);
601 g_object_unref(f);
602 } else {
603 maximum_width = maximum_width_;
604 }
605
606 if(maximum_width_ != -1) {
607 maximum_width = std::min(maximum_width, maximum_width_);
608 }
609
610 pango_layout_set_width(&layout, maximum_width == -1
611 ? -1
612 : maximum_width * PANGO_SCALE);
613 pango_layout_get_pixel_extents(&layout, nullptr, &size);
614
615 DBG_GUI_L << "pango_text::" << __func__
616 << " text '" << gui2::debug_truncate(text_)
617 << "' maximum_width " << maximum_width
618 << " width " << size.x + size.width
619 << ".";
620
621 DBG_GUI_L << "pango_text::" << __func__
622 << " text '" << gui2::debug_truncate(text_)
623 << "' font_size " << font_size_
624 << " markedup_text " << markedup_text_
625 << " font_style " << std::hex << font_style_ << std::dec
626 << " maximum_width " << maximum_width
627 << " maximum_height " << maximum_height_
628 << " result " << size
629 << ".";
630
631 if(maximum_width != -1 && size.x + size.width > maximum_width) {
632 DBG_GUI_L << "pango_text::" << __func__
633 << " text '" << gui2::debug_truncate(text_)
634 << " ' width " << size.x + size.width
635 << " greater as the wanted maximum of " << maximum_width
636 << ".";
637 }
638
639 // The maximum height is handled here instead of using the library - see the comments in set_maximum_height()
640 if(maximum_height_ != -1 && size.y + size.height > maximum_height_) {
641 DBG_GUI_L << "pango_text::" << __func__
642 << " text '" << gui2::debug_truncate(text_)
643 << " ' height " << size.y + size.height
644 << " greater as the wanted maximum of " << maximum_height_
645 << ".";
646 size.height = maximum_height_ - std::max(0, size.y);
647 }
648
649 return size;
650}
651
652/***
653 * Inverse table
654 *
655 * Holds a high-precision inverse for each number i, that is, a number x such that x * i / 256 is close to 255.
656 */
658{
659 unsigned values[256] {};
660
661 constexpr inverse_table()
662 {
663 values[0] = 0;
664 for (int i = 1; i < 256; ++i) {
665 values[i] = (255 * 256) / i;
666 }
667 }
668
669 unsigned operator[](uint8_t i) const { return values[i]; }
670};
671
673
674/***
675 * Helper function for un-premultiplying alpha
676 * Div should be the high-precision inverse for the alpha value.
677 */
678static void unpremultiply(uint8_t & value, const unsigned div) {
679 unsigned temp = (value * div) / 256u;
680 // Note: It's always the case that alpha * div < 256 if div is the inverse
681 // for alpha, so if cairo is computing premultiplied alpha by rounding down,
682 // this min is not necessary. However, if cairo generates illegal output,
683 // the min may be selected.
684 // It's probably not worth removing the min, since branch prediction will
685 // make it essentially free if one of the branches is never actually
686 // selected.
687 value = std::min(255u, temp);
688}
689
690/**
691 * Converts from cairo-format ARGB32 premultiplied alpha to plain alpha.
692 * @param c a uint32 representing the color
693 */
694static void from_cairo_format(uint32_t & c)
695{
696 uint8_t a = (c >> 24) & 0xff;
697 uint8_t r = (c >> 16) & 0xff;
698 uint8_t g = (c >> 8) & 0xff;
699 uint8_t b = c & 0xff;
700
701 const unsigned div = inverse_table_[a];
702 unpremultiply(r, div);
703 unpremultiply(g, div);
704 unpremultiply(b, div);
705
706 c = (static_cast<uint32_t>(a) << 24) | (static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | static_cast<uint32_t>(b);
707}
708
709void pango_text::render(PangoLayout& layout, const rect& viewport)
710{
711 auto cairo_surface = cairo::create_surface(&surface_buffer_[0], point{ viewport.w, viewport.h }); // TODO: use rect::size
712 auto cairo_context = cairo::create_context(cairo_surface);
713
714 // Convenience pointer
715 cairo_t* cr = cairo_context.get();
716
717 if(cairo_status(cr) == CAIRO_STATUS_INVALID_SIZE) {
718 throw std::length_error("Text is too long to render");
719 }
720
721 // The top-left of the text, which can be outside the area to be rendered
722 cairo_move_to(cr, -viewport.x, -viewport.y);
723
724 //
725 // TODO: the outline may be slightly cut off around certain text if it renders too
726 // close to the surface's edge. That causes the outline to extend just slightly
727 // outside the surface's borders. I'm not sure how best to deal with this. Obviously,
728 // we want to increase the surface size, but we also don't want to invalidate all
729 // the placement and size calculations. Thankfully, it's not very noticeable.
730 //
731 // -- vultraz, 2018-03-07
732 //
733 if(add_outline_) {
734 // Add a path to the cairo context tracing the current text.
735 pango_cairo_layout_path(cr, &layout);
736
737 // Set color for background outline (black).
738 cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 1.0);
739
740 cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
741 cairo_set_line_width(cr, 3.0); // Adjust as necessary
742
743 // Stroke path to draw outline.
744 cairo_stroke(cr);
745 }
746
747 // Set main text color.
748 cairo_set_source_rgba(cr,
749 foreground_color_.r / 255.0,
750 foreground_color_.g / 255.0,
751 foreground_color_.b / 255.0,
752 foreground_color_.a / 255.0
753 );
754
757 list.insert(pango_attr_underline_new(PANGO_UNDERLINE_SINGLE));
758 apply_attributes(list);
759 }
760
761 pango_cairo_show_layout(cr, &layout);
762}
763
765{
766 return create_surface({0, 0, rect_.x + rect_.width, rect_.y + rect_.height});
767}
768
770{
771 assert(layout_.get());
772
773 cairo_format_t format = CAIRO_FORMAT_ARGB32;
774 const int stride = cairo_format_stride_for_width(format, viewport.w);
775
776 // The width and stride can be zero if the text is empty or the stride can be negative to indicate an error from
777 // Cairo. Width isn't tested here because it's implied by stride.
778 if(stride <= 0 || viewport.h <= 0) {
779 surface_buffer_.clear();
780 return nullptr;
781 }
782
783 DBG_FT << "creating new text surface";
784
785 // Check to prevent arithmetic overflow when calculating (stride * height).
786 // The size of the viewport should already provide a far lower limit on the
787 // maximum size, but this is left in as a sanity check.
788 if(viewport.h > std::numeric_limits<int>::max() / stride) {
789 throw std::length_error("Text is too long to render");
790 }
791
792 // Resize buffer appropriately and set all pixel values to 0.
793 surface_buffer_.assign(viewport.h * stride, 0);
794
795 // Try rendering the whole text in one go. If this throws a length_error
796 // then leave it to the caller to handle; one reason it may throw is that
797 // cairo surfaces are limited to approximately 2**15 pixels in height.
798 render(*layout_, viewport);
799
800 // The cairo surface is in CAIRO_FORMAT_ARGB32 which uses
801 // pre-multiplied alpha. SDL doesn't use that so the pixels need to be
802 // decoded again.
803 for(int y = 0; y < viewport.h; ++y) {
804 uint32_t* pixels = reinterpret_cast<uint32_t*>(&surface_buffer_[y * stride]);
805 for(int x = 0; x < viewport.w; ++x) {
806 from_cairo_format(pixels[x]);
807 }
808 }
809
810 return SDL_CreateSurfaceFrom(viewport.w, viewport.h, SDL_PIXELFORMAT_ARGB8888, &surface_buffer_[0], stride);
811}
812
813bool pango_text::set_markup(std::string_view text, PangoLayout& layout)
814{
815 char* raw_text;
816 std::string semi_escaped;
817 bool valid = validate_markup(text, &raw_text, semi_escaped);
818 if(!semi_escaped.empty()) {
819 text = semi_escaped;
820 }
821
822 if(valid) {
823 if(link_aware_) {
824 std::string formatted_text = format_links(text);
825 pango_layout_set_markup(&layout, formatted_text.c_str(), formatted_text.size());
826 } else {
827 pango_layout_set_markup(&layout, text.data(), text.size());
828 }
829 }
830
831 return valid;
832}
833
834/**
835 * Replaces all instances of URLs in a given string with formatted links
836 * and returns the result.
837 */
838std::string pango_text::format_links(std::string_view text) const
839{
840 static const std::string delim = " \n\r\t";
841 std::ostringstream result;
842
843 std::size_t tok_start = 0;
844 for(std::size_t pos = 0; pos < text.length(); ++pos) {
845 if(delim.find(text[pos]) == std::string::npos) {
846 continue;
847 }
848
849 if(const auto tok_length = pos - tok_start) {
850 // Token starts from after the last delimiter up to (but not including) this delimiter
851 auto token = text.substr(tok_start, tok_length);
852 if(looks_like_url(token)) {
853 result << format_as_link(std::string{token}, link_color_);
854 } else {
855 result << token;
856 }
857 }
858
859 result << text[pos];
860 tok_start = pos + 1;
861 }
862
863 // Deal with the remainder token
864 if(tok_start < text.length()) {
865 auto token = text.substr(tok_start);
866 if(looks_like_url(token)) {
867 result << format_as_link(std::string{token}, link_color_);
868 } else {
869 result << token;
870 }
871 }
872
873 return result.str();
874}
875
876bool pango_text::validate_markup(std::string_view text, char** raw_text, std::string& semi_escaped) const
877{
878 if(pango_parse_markup(text.data(), text.size(),
879 0, nullptr, raw_text, nullptr, nullptr)) {
880 return true;
881 }
882
883 /*
884 * The markup is invalid. Try to recover.
885 *
886 * The pango engine tested seems to accept stray single quotes »'« and
887 * double quotes »"«. Stray ampersands »&« seem to give troubles.
888 * So only try to recover from broken ampersands, by simply replacing them
889 * with the escaped version.
890 */
891 semi_escaped = semi_escape_text(text);
892
893 /*
894 * If at least one ampersand is replaced the semi-escaped string
895 * is longer than the original. If this isn't the case then the
896 * markup wasn't (only) broken by ampersands in the first place.
897 */
898 if(text.size() == semi_escaped.size()
899 || !pango_parse_markup(semi_escaped.c_str(), semi_escaped.size()
900 , 0, nullptr, raw_text, nullptr, nullptr)) {
901
902 /* Fixing the ampersands didn't work. */
903 return false;
904 }
905
906 /* Replacement worked, still warn the user about the error. */
907 WRN_GUI_L << "pango_text::" << __func__
908 << " text '" << text
909 << "' has unescaped ampersands '&', escaped them.";
910
911 return true;
912}
913
914std::vector<std::string> pango_text::get_lines() const
915{
916 recalculate();
917
918 PangoLayout* const layout = layout_.get();
919 std::vector<std::string> res;
920 int count = pango_layout_get_line_count(layout);
921
922 if(count < 1) {
923 return res;
924 }
925
926 using layout_iterator = std::unique_ptr<PangoLayoutIter, std::function<void(PangoLayoutIter*)>>;
927 layout_iterator i{pango_layout_get_iter(layout), pango_layout_iter_free};
928
929 res.reserve(count);
930
931 do {
932 PangoLayoutLine* ll = pango_layout_iter_get_line_readonly(i.get());
933 const char* begin = &pango_layout_get_text(layout)[ll->start_index];
934 res.emplace_back(begin, ll->length);
935 } while(pango_layout_iter_next_line(i.get()));
936
937 return res;
938}
939
940PangoLayoutLine* pango_text::get_line(int index)
941{
942 return pango_layout_get_line_readonly(layout_.get(), index);
943}
944
945std::pair<int, int> pango_text::index_to_line_x(const unsigned offset, const bool trailing) const
946{
947 int line_num = 0, x_pos = 0;
948 pango_layout_index_to_line_x(layout_.get(), offset, trailing, &line_num, &x_pos);
949 return { line_num, x_pos };
950}
951
953{
954 static pango_text text_renderer;
955 return text_renderer;
956}
957
959{
960 // Reset metrics to defaults
961 return get_text_renderer()
962 .set_family_class(fclass)
963 .set_font_style(style)
964 .set_font_size(size)
966}
967
968} // namespace font
double t
Helper class to encapsulate the management of a PangoAttrList.
void splice_into(PangoAttrList *target) const
void apply_to(PangoLayout *layout) const
void insert(PangoAttribute *attr)
Small helper class to make sure the pango font object is destroyed properly.
Definition font.hpp:25
Text class.
Definition text.hpp:78
const std::string & text() const
Definition text.hpp:322
int pixel_scale_
The pixel scale, used to render high-DPI text.
Definition text.hpp:459
pango_text & set_font_style(const FONT_STYLE font_style)
Definition text.cpp:381
PangoEllipsizeMode ellipse_mode_
The way too long text is shown depends on this mode.
Definition text.hpp:438
bool add_outline_
Whether to add an outline effect.
Definition text.hpp:399
void clear_attributes()
Definition text.cpp:319
bool validate_markup(std::string_view text, char **raw_text, std::string &semi_escaped) const
Definition text.cpp:876
bool set_markup(std::string_view text, PangoLayout &layout)
Sets the markup'ed text.
Definition text.cpp:813
pango_text & set_maximum_length(const std::size_t maximum_length)
Definition text.cpp:482
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:764
PangoAlignment alignment_
The alignment of the text.
Definition text.hpp:441
PangoRectangle rect_
Definition text.hpp:366
int maximum_height_
The maximum height of the text.
Definition text.hpp:435
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:416
color_t link_color_
The color to render links in.
Definition text.hpp:384
unsigned insert_text(const unsigned offset, const std::string &text, const bool use_markup=false)
Inserts UTF-8 text.
Definition text.cpp:169
void recalculate() const
Recalculates the text layout.
Definition text.cpp:565
color_t foreground_color_
The foreground color.
Definition text.hpp:396
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:364
bool link_aware_
Are hyperlinks in the text marked-up, and will get_link return them.
Definition text.hpp:375
pango_text & set_foreground_color(const color_t &color)
Definition text.cpp:391
int to_draw_scale(int s) const
Scale the given render-space size to draw-space, rounding up.
Definition text.cpp:143
texture render_texture(const rect &viewport)
Wrapper around render_surface which sets texture::w() and texture::h() in the same way that render_an...
Definition text.cpp:117
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:838
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:940
unsigned characters_per_line_
The number of characters per line.
Definition text.hpp:427
bool markedup_text_
Does the text contain pango markup? If different render routines must be used.
Definition text.hpp:372
pango_text & set_family_class(font::family_class fclass)
Definition text.cpp:359
font::family_class font_class_
The font family class used.
Definition text.hpp:387
void apply_attributes(const font::attribute_list &attrs)
Definition text.cpp:324
std::vector< std::string > get_lines() const
Retrieves a list of strings with contents for each rendered line.
Definition text.cpp:914
void update_pixel_scale()
Update pixel scale, if necessary.
Definition text.cpp:544
std::tuple< int, int, bool > xy_to_index(const point &position) const
Wrapper function around pango_layout_xy_to_index.
Definition text.cpp:308
unsigned font_size_
The font size to draw.
Definition text.hpp:390
std::vector< uint8_t > surface_buffer_
Buffer to store the image on.
Definition text.hpp:504
pango_text & set_add_outline(bool do_add)
Definition text.cpp:514
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:452
PangoRectangle calculate_size(PangoLayout &layout) const
Calculates surface size.
Definition text.cpp:579
pango_text & set_alignment(const PangoAlignment alignment)
Definition text.cpp:472
std::string text_
The text to draw (stored as UTF-8).
Definition text.hpp:369
std::pair< int, int > index_to_line_x(const unsigned offset, const bool trailing=0) const
Given a byte index, find out at which line the corresponding character is located.
Definition text.cpp:945
bool calculation_dirty_
The text has two dirty states:
Definition text.hpp:453
std::unique_ptr< PangoLayout, std::function< void(void *)> > layout_
Definition text.hpp:365
pango_text & set_font_size(unsigned font_size)
Definition text.cpp:369
pango_text & set_link_aware(bool b)
Definition text.cpp:495
void render(PangoLayout &layout, const rect &viewport)
This is part of create_surface(viewport).
Definition text.cpp:709
FONT_STYLE font_style_
The style of the font, this is an orred mask of the font flags.
Definition text.hpp:393
bool set_text(const std::string &text, const bool markedup)
Sets the text to render.
Definition text.cpp:333
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
surface render_surface(const rect &viewport)
Returns the rendered text.
Definition text.cpp:129
std::size_t maximum_length_
The maximum length of the text.
Definition text.hpp:444
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:427
pango_text & set_maximum_width(int width)
Definition text.cpp:400
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:504
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
std::size_t length_
Length of the text.
Definition text.hpp:456
int get_max_glyph_height() const
Returns the maximum glyph height of a font, in drawing coordinates.
Definition text.cpp:524
int maximum_width_
The maximum width of the text.
Definition text.hpp:409
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
#define DBG_FT
static std::string _(const char *str)
Definition gettext.hpp:100
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
Graphical text output.
pango_text & get_text_renderer()
Returns a reference to a static pango_text object.
Definition text.cpp:952
constexpr float get_line_spacing_factor()
Definition text.hpp:568
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.
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:958
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:678
static void from_cairo_format(uint32_t &c)
Converts from cairo-format ARGB32 premultiplied alpha to plain alpha.
Definition text.cpp:694
static constexpr inverse_table inverse_table_
Definition text.cpp:672
std::string_view debug_truncate(std::string_view text)
Returns a truncated version of the text.
Definition helper.cpp:172
STL namespace.
std::string & insert(std::string &str, const std::size_t pos, const std::string &insert)
Insert a UTF-8 string at the specified position.
Definition unicode.cpp:100
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition unicode.cpp:81
std::string & truncate(std::string &str, const std::size_t size)
Truncates a UTF-8 string to the specified number of characters.
Definition unicode.cpp:118
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:480
int w
Definition pathfind.cpp:188
std::unique_ptr< MIX_Audio, decltype(&MIX_DestroyAudio)> value
Definition sound.cpp:139
The basic class for representing 8-bit RGB or RGBA colour values.
Definition color.hpp:51
unsigned values[256]
Definition text.cpp:659
constexpr inverse_table()
Definition text.cpp:661
unsigned operator[](uint8_t i) const
Definition text.cpp:669
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_party p
static lg::log_domain log_font("font")
#define i
#define g
#define f
#define c
#define b