Root/src/font.cpp

1#include "font.h"
2
3#include "debug.h"
4#include "surface.h"
5#include "utilities.h"
6
7#include <SDL.h>
8#include <SDL_ttf.h>
9#include <algorithm>
10#include <vector>
11
12/* TODO: Let the theme choose the font and font size */
13#define TTF_FONT "/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed.ttf"
14#define TTF_FONT_SIZE 12
15
16using namespace std;
17
18unique_ptr<Font> Font::defaultFont()
19{
20    return unique_ptr<Font>(new Font(TTF_FONT, TTF_FONT_SIZE));
21}
22
23Font::Font(const std::string &path, unsigned int size)
24{
25    font = nullptr;
26    lineSpacing = 1;
27
28    /* Note: TTF_Init and TTF_Quit perform reference counting, so call them
29     * both unconditionally for each font. */
30    if (TTF_Init() < 0) {
31        ERROR("Unable to init SDL_ttf library\n");
32        return;
33    }
34
35    font = TTF_OpenFont(path.c_str(), size);
36    if (!font) {
37        ERROR("Unable to open font '%s'\n", TTF_FONT);
38        TTF_Quit();
39        return;
40    }
41
42    lineSpacing = TTF_FontLineSkip(font);
43}
44
45Font::~Font()
46{
47    if (font) {
48        TTF_CloseFont(font);
49        TTF_Quit();
50    }
51}
52
53int Font::getTextWidth(const string &text)
54{
55    if (!font) {
56        return 1;
57    }
58
59    int w;
60    size_t pos = text.find('\n', 0);
61    if (pos == string::npos) {
62        TTF_SizeUTF8(font, text.c_str(), &w, nullptr);
63        return w;
64    } else {
65        int maxWidth = 1;
66        size_t prev = 0;
67        do {
68            TTF_SizeUTF8(font, text.substr(prev, pos - prev).c_str(), &w, nullptr);
69            maxWidth = max(w, maxWidth);
70            prev = pos + 1;
71            pos = text.find('\n', prev);
72        } while (pos != string::npos);
73        TTF_SizeUTF8(font, text.substr(prev).c_str(), &w, nullptr);
74        return max(w, maxWidth);
75    }
76}
77
78string Font::wordWrap(const string &text, int width)
79{
80    const size_t len = text.length();
81    string result;
82    result.reserve(len);
83
84    size_t start = 0;
85    while (true) {
86        size_t end = min(text.find('\n', start), len);
87        result.append(wordWrapSingleLine(text, start, end, width));
88        start = end + 1;
89        if (start >= len) {
90            break;
91        }
92        result.push_back('\n');
93    }
94
95    return result;
96}
97
98string Font::wordWrapSingleLine(const string &text, size_t start, size_t end, int width)
99{
100    string result;
101    result.reserve(end - start);
102
103    while (start != end) {
104        /* Clean the end of the string, allowing lines that are indented at
105         * the start to stay as such. */
106        string run = rtrim(text.substr(start, end - start));
107        int runWidth = getTextWidth(run);
108
109        if (runWidth > width) {
110            size_t fits = 0, doesntFit = run.length();
111            /* First guess: width / runWidth approximates the proportion of
112             * the run that should fit. */
113            size_t guess = min(run.length(), (size_t) (doesntFit * ((float) width / runWidth)));
114            /* Adjust that to fully include any partial UTF-8 character. */
115            while (guess < run.length() && !isUTF8Starter(run[guess])) {
116                guess++;
117            }
118
119            if (getTextWidth(run.substr(0, guess)) <= width) {
120                fits = guess;
121                doesntFit = fits;
122                /* Prime doesntFit, which should be closer to 2 * fits than
123                 * to run.length() / 2 if the run is long. */
124                do {
125                    fits = doesntFit; // determined to fit by a previous iteration
126                    doesntFit = min(2 * fits, run.length());
127                    while (doesntFit < run.length() && !isUTF8Starter(run[doesntFit])) {
128                        doesntFit++;
129                    }
130                } while (doesntFit < run.length() && getTextWidth(run.substr(0, doesntFit)) <= width);
131            } else {
132                doesntFit = guess;
133            }
134
135            /* End this loop when N full characters fit but N + 1 don't. */
136            while (fits + 1 < doesntFit) {
137                size_t guess = fits + (doesntFit - fits) / 2;
138                if (!isUTF8Starter(run[guess])) {
139                    size_t oldGuess = guess;
140                    /* Adjust the guess to fully include a UTF-8 character. */
141                    for (size_t offset = 1; offset < (doesntFit - fits) / 2 - 1; offset++) {
142                        if (isUTF8Starter(run[guess - offset])) {
143                            guess -= offset;
144                            break;
145                        } else if (isUTF8Starter(run[guess + offset])) {
146                            guess += offset;
147                            break;
148                        }
149                    }
150                    /* If there's no such character, exit early. */
151                    if (guess == oldGuess) {
152                        break;
153                    }
154                }
155                if (getTextWidth(run.substr(0, guess)) <= width) {
156                    fits = guess;
157                } else {
158                    doesntFit = guess;
159                }
160            }
161
162            /* The run shall be split at the last space-separated word that
163             * fully fits, or otherwise at the last character that fits. */
164            size_t lastSpace = run.find_last_of(" \t\r", fits);
165            if (lastSpace != string::npos) {
166                fits = lastSpace;
167            }
168
169            /* If 0 characters fit, we'll have to make 1 fit anyway, otherwise
170             * we're in for an infinite loop. This can happen if the font size
171             * is large. */
172            if (fits == 0) {
173                fits = 1;
174                while (fits < run.length() && !isUTF8Starter(run[fits])) {
175                    fits++;
176                }
177            }
178
179            result.append(rtrim(run.substr(0, fits))).append("\n");
180            start = min(end, text.find_first_not_of(" \t\r", start + fits));
181        } else {
182            result.append(rtrim(run));
183            start = end;
184        }
185    }
186
187    return result;
188}
189
190int Font::getTextHeight(const string &text)
191{
192    int nLines = 1;
193    size_t pos = 0;
194    while ((pos = text.find('\n', pos)) != string::npos) {
195        nLines++;
196        pos++;
197    }
198    return nLines * getLineSpacing();
199}
200
201int Font::write(Surface& surface, const string &text,
202            int x, int y, HAlign halign, VAlign valign)
203{
204    if (!font) {
205        return 0;
206    }
207
208    size_t pos = text.find('\n', 0);
209    if (pos == string::npos) {
210        return writeLine(surface, text, x, y, halign, valign);
211    } else {
212        int maxWidth = 0;
213        size_t prev = 0;
214        do {
215            maxWidth = max(maxWidth,
216                    writeLine(surface, text.substr(prev, pos - prev),
217                        x, y, halign, valign));
218            y += lineSpacing;
219            prev = pos + 1;
220            pos = text.find('\n', prev);
221        } while (pos != string::npos);
222        return max(maxWidth,
223                writeLine(surface, text.substr(prev), x, y, halign, valign));
224    }
225}
226
227int Font::writeLine(Surface& surface, std::string const& text,
228                int x, int y, HAlign halign, VAlign valign)
229{
230    if (text.empty()) {
231        // SDL_ttf will return a nullptr when rendering the empty string.
232        return 0;
233    }
234
235    switch (valign) {
236    case VAlignTop:
237        break;
238    case VAlignMiddle:
239        y -= lineSpacing / 2;
240        break;
241    case VAlignBottom:
242        y -= lineSpacing;
243        break;
244    }
245
246    SDL_Color color = { 0, 0, 0, 0 };
247    SDL_Surface *s = TTF_RenderUTF8_Blended(font, text.c_str(), color);
248    if (!s) {
249        ERROR("Font rendering failed for text \"%s\"\n", text.c_str());
250        return 0;
251    }
252    const int width = s->w;
253
254    switch (halign) {
255    case HAlignLeft:
256        break;
257    case HAlignCenter:
258        x -= width / 2;
259        break;
260    case HAlignRight:
261        x -= width;
262        break;
263    }
264
265    SDL_Rect rect = { (Sint16) x, (Sint16) (y - 1), 0, 0 };
266    SDL_BlitSurface(s, NULL, surface.raw, &rect);
267
268    /* Note: rect.x / rect.y are reset everytime because SDL_BlitSurface
269     * will modify them if negative */
270    rect.x = x;
271    rect.y = y + 1;
272    SDL_BlitSurface(s, NULL, surface.raw, &rect);
273
274    rect.x = x - 1;
275    rect.y = y;
276    SDL_BlitSurface(s, NULL, surface.raw, &rect);
277
278    rect.x = x + 1;
279    rect.y = y;
280    SDL_BlitSurface(s, NULL, surface.raw, &rect);
281    SDL_FreeSurface(s);
282
283    rect.x = x;
284    rect.y = y;
285    color.r = 0xff;
286    color.g = 0xff;
287    color.b = 0xff;
288
289    s = TTF_RenderUTF8_Blended(font, text.c_str(), color);
290    if (!s) {
291        ERROR("Font rendering failed for text \"%s\"\n", text.c_str());
292        return width;
293    }
294    SDL_BlitSurface(s, NULL, surface.raw, &rect);
295    SDL_FreeSurface(s);
296
297    return width;
298}
299

Archive Download this file



interactive