-
-
Notifications
You must be signed in to change notification settings - Fork 988
/
text.hpp
491 lines (402 loc) · 15.1 KB
/
text.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
/*
Copyright (C) 2008 - 2018 by Mark de Wever <koraq@xs4all.nl>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "font/font_options.hpp"
#include "color.hpp"
#include "sdl/surface.hpp"
#include "sdl/texture.hpp"
#include "serialization/string_utils.hpp"
#include "serialization/unicode_types.hpp"
#include <pango/pango.h>
#include <pango/pangocairo.h>
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <vector>
/**
* Note: This is the cairo-pango code path, not the SDL_TTF code path.
*/
struct language_def;
struct point;
namespace font
{
// add background color and also font markup.
/**
* Text class.
*
* This class represents text which is rendered using Pango.
*
* It takes text, as a utf-8 std::string, plus formatting options including
* font and color. It provides a surface object which holds the rendered text.
*
* Besides this, it can do some additional calculations using the font layout.
*
* It can take an index into the text, and convert it to pixel coordinates,
* so that if we want to draw a cursor in an editbox, we know where to draw it.
*
* It can also take a pixel coordinate with respect to the text layout, and
* translate it back to an index into the original text. This is useful if the
* user clicks on the text, and we want to know where to move the cursor.
*
* The get_token method takes a pixel coordinate, which we assume represents a
* click position, and gets the corresponding "token" from the string. The default
* token delimiters are whitespace " \n\r\t". So, this returns the "word" that the
* user clicked on.
*
* Finally, the get_link method represents special support for hyperlinks in text.
* A token "looks like a link" if it begins "http://" or "https://".
* If a text has link_aware enabled, then any such token is rendered with an
* underline and in a special color, see `link_color`.
* The get_link method calls get_token and further checks if the clicked token
* looks like a link.
*
* This class stores the text to draw and uses pango with the cairo backend to
* render the text. See http://pango.org for more info.
*
*/
class pango_text
{
public:
pango_text();
pango_text(const pango_text &) = delete;
pango_text & operator = (const pango_text &) = delete;
/**
* Returns the rendered text texture from the cache.
*
* If the surface is flagged dirty it will first be re-rendered and a new
* texture added to the cache upon redraw.
*/
texture& render_and_get_texture();
/**
* Returns the rendered text surface directly.
*
* If the surface is flagged dirty it will first be re-rendered and a new
* texture added to the cache upon redraw.
*/
surface& render_and_get_surface();
/** Returns the width needed for the text. */
int get_width() const;
/** Returns the height needed for the text. */
int get_height() const;
/** Returns the pixel size needed for the text. */
point get_size() const;
/** Has the text been truncated? This happens if it exceeds max width or height. */
bool is_truncated() const;
/**
* Inserts UTF-8 text.
*
* @param offset The position to insert the text.
* @param text The UTF-8 text to insert.
*
* @returns The number of characters inserted.
*/
unsigned insert_text(const unsigned offset, const std::string& text);
/**
* Inserts a unicode char.
*
* @param offset The position to insert the char.
* @param unicode The character to insert.
*
* @returns True upon success, false otherwise.
*/
bool insert_unicode(const unsigned offset, ucs4::char_t unicode);
/**
* Inserts unicode text.
*
* @param offset The position to insert the text.
* @param unicode Vector with characters to insert.
*
* @returns The number of characters inserted.
*/
unsigned insert_unicode(
const unsigned offset, const ucs4::string& unicode);
/***** ***** ***** ***** Font flags ***** ***** ***** *****/
// NOTE: these values must be powers of 2 in order to be bit-unique
enum FONT_STYLE {
STYLE_NORMAL = 0,
STYLE_BOLD = 1,
STYLE_ITALIC = 2,
STYLE_UNDERLINE = 4,
};
/***** ***** ***** ***** Query details ***** ***** ***** *****/
/**
* Gets the location for the cursor.
*
* @param column The column offset of the cursor.
* @param line The line offset of the cursor.
*
* @returns The position of the top of the cursor. It the
* requested location is out of range 0,0 is
* returned.
*/
point get_cursor_position(
const unsigned column, const unsigned line = 0) const;
/**
* Gets the largest collection of characters, including the token at position,
* and not including any characters from the delimiters set.
*
* @param position The pixel position in the text area.
*
* @returns The token containing position, and none of the
* delimiter characters. If position is out of bounds,
* it returns the empty string.
*/
std::string get_token(const point & position, const char * delimiters = " \n\r\t") const;
/**
* Checks if position points to a character in a link in the text, returns it
* if so, empty string otherwise. Link-awareness must be enabled to get results.
* @param position The pixel position in the text area.
*
* @returns The link if one is found, the empty string otherwise.
*/
std::string get_link(const point & position) const;
/**
* Gets the column of line of the character at the position.
*
* @param position The pixel position in the text area.
*
* @returns A point with the x value the column and the y
* value the line of the character found (or last
* character if not found.
*/
point get_column_line(const point& position) const;
/**
* Gets the length of the text in bytes.
*
* The text set is UTF-8 so the length of the string might not be the length
* of the text.
*/
size_t get_length() const { return length_; }
/**
* Sets the text to render.
*
* @param text The text to render.
* @param markedup Should the text be rendered with pango
* markup. If the markup is invalid it's
* rendered as text without markup.
*
* @returns The status, if rendered as markup and the
* markup contains errors, false is returned
* else true.
*/
bool set_text(const std::string& text, const bool markedup);
/***** ***** ***** ***** Setters / getters ***** ***** ***** *****/
const std::string& text() const { return text_; }
pango_text& set_family_class(font::family_class fclass);
pango_text& set_font_size(const unsigned font_size);
pango_text& set_font_style(const FONT_STYLE font_style);
pango_text& set_foreground_color(const color_t& color);
pango_text& set_maximum_width(int width);
pango_text& set_characters_per_line(const unsigned characters_per_line);
pango_text& set_maximum_height(int height, bool multiline);
pango_text& set_ellipse_mode(const PangoEllipsizeMode ellipse_mode);
pango_text& set_alignment(const PangoAlignment alignment);
pango_text& set_maximum_length(const size_t maximum_length);
bool link_aware() const { return link_aware_; }
pango_text& set_link_aware(bool b);
pango_text& set_link_color(const color_t& color);
pango_text& set_add_outline(bool do_add);
private:
/***** ***** ***** ***** Pango variables ***** ***** ***** *****/
std::unique_ptr<PangoContext, std::function<void(void*)>> context_;
std::unique_ptr<PangoLayout, std::function<void(void*)>> layout_;
mutable PangoRectangle rect_;
// Used if the text is too long to fit into a single Cairo surface.
std::vector<std::unique_ptr<PangoLayout, std::function<void(void*)>>> sublayouts_;
/** The SDL surface to render upon used as a cache. */
mutable surface surface_;
/** The text to draw (stored as UTF-8). */
std::string text_;
/** Does the text contain pango markup? If different render routines must be used. */
bool markedup_text_;
/** Are hyperlinks in the text marked-up, and will get_link return them. */
bool link_aware_;
/**
* The color to render links in.
*
* Links are formatted using pango <span> as follows:
*
* <span underline="single" color=" + link_color_ + ">
*/
color_t link_color_;
/** The font family class used. */
font::family_class font_class_;
/** The font size to draw. */
unsigned font_size_;
/** The style of the font, this is an orred mask of the font flags. */
FONT_STYLE font_style_;
/** The foreground color. */
color_t foreground_color_;
/** Whether to add an outline effect. */
bool add_outline_;
/**
* The maximum width of the text.
*
* Values less or equal to 0 mean no maximum and are internally stored as
* -1, since that's the value pango uses for it.
*
* See @ref characters_per_line_.
*/
int maximum_width_;
/**
* The number of characters per line.
*
* This can be used as an alternative of @ref maximum_width_. The user can
* select a number of characters on a line for wrapping. When the value is
* non-zero it determines the maximum width based on the average character
* width.
*
* If both @ref maximum_width_ and @ref characters_per_line_ are set the
* minimum of the two will be the maximum.
*
* @note Long lines are often harder to read, setting this value can
* automatically wrap on a number of characters regardless of the font
* size. Often 66 characters is considered the optimal value for a one
* column text.
*/
unsigned characters_per_line_;
/**
* The maximum height of the text.
*
* Values less or equal to 0 mean no maximum and are internally stored as
* -1, since that's the value pango uses for it.
*/
int maximum_height_;
/** The way too long text is shown depends on this mode. */
PangoEllipsizeMode ellipse_mode_;
/** The alignment of the text. */
PangoAlignment alignment_;
/** The maximum length of the text. */
size_t maximum_length_;
/**
* The text has two dirty states:
* - The setting of the state and the size calculations.
* - The rendering of the surface.
*/
/** The dirty state of the calculations. */
mutable bool calculation_dirty_;
/** Length of the text. */
mutable size_t length_;
/**
* Recalculates the text layout.
*
* When the text is recalculated the surface is dirtied.
*
* @param force Recalculate even if not dirty?
*/
void recalculate(const bool force = false) const;
/** Calculates surface size. */
PangoRectangle calculate_size(PangoLayout& layout) const;
/** The dirty state of the surface. */
mutable bool surface_dirty_;
/**
* Renders the text.
*
* It will do a recalculation first so no need to call both.
*
* @param force Render even if not dirty? This parameter is
* also send to recalculate().
*/
void rerender(const bool force = false);
void render(PangoLayout& layout, const PangoRectangle& rect,
const size_t surface_buffer_offset, const unsigned stride);
/**
* Buffer to store the image on.
*
* We use a cairo surface to draw on this buffer and then use the buffer as
* data source for the SDL_Surface. This means the buffer needs to be stored
* in the object, since SDL_Surface doesn't own its buffer.
*/
mutable std::vector<uint8_t> surface_buffer_;
/**
* Creates a new buffer.
*
* If needed frees the other surface and then creates a new buffer and
* initializes the entire buffer with values 0.
*
* NOTE even though we're clearly modifying function we don't change the
* state of the object. The const is needed so other functions can also be
* marked const (those also don't change the state of the object.
*
* @param size The required size of the buffer.
*/
void create_surface_buffer(const size_t size) const;
/**
* Sets the markup'ed text.
*
* It tries to set the text as markup. If the markup is invalid it will try
* a bit harder to recover from the errors and still set the markup.
*
* @param text The text to set as markup.
*
* @returns Whether the markup was set or an
* unrecoverable error occurred and the text is
* set as plain text with an error message.
*/
bool set_markup(utils::string_view text, PangoLayout& layout);
bool validate_markup(utils::string_view text, char** raw_text, std::string& semi_escaped) const;
/** Splits the text to two Cairo surfaces.
*
* The implementation isn't recursive: the function only splits the text once.
* As a result, it only doubles the maximum surface height to 64,000 pixels
* or so.
* The reason for this is that a recursive implementation would be more complex
* and it's unnecessary for now, as the longest surface in the game
* (end credits) is only about 40,000 pixels high with the default_large widget
* definition.
* If we need even larger surfaces in the future, the implementation can be made
* recursive.
*/
void split_surface();
bool is_surface_split() const
{
return sublayouts_.size() > 0;
}
static void copy_layout_properties(PangoLayout& src, PangoLayout& dst);
std::vector<std::string> find_links(utils::string_view text) const;
void format_links(std::string& text, const std::vector<std::string>& links) const;
/** Hash for the current settings (text, size, etc) configuration. */
size_t hash_;
// Allow specialization of std::hash for pango_text
friend struct std::hash<pango_text>;
};
/**
* Returns a reference to a static pango_text object.
*
* Since the class is essentially a render pipeline, there's no need for individual
* areas of the game to own their own renderers. Not to mention it isn't a trivial
* class; constructing one is likely to be expensive.
*/
pango_text& get_text_renderer();
using pango_text_cache_t = std::map<size_t, texture>;
/**
* The text texture cache.
*
* Each time a specific bit of text is rendered, a corresponding texture is created and
* added to the cache. We don't store the surface since there isn't really any use for
* it. If we need texture size that can be easily queried.
*
* @todo Figure out how this can be optimized with a texture atlas. It should be possible
* to store smaller bits of text in the atlas and construct new textures from them.
*/
static pango_text_cache_t rendered_text_cache;
} // namespace font
// Specialize std::hash for pango_text
namespace std
{
template<>
struct hash<font::pango_text>
{
size_t operator()(const font::pango_text& t) const;
};
} // namespace std