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