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