Root/
Source at commit 73ceffa51d3450d2d20ce933e7fc6da187e09fc8 created 8 years 11 months ago. By Maarten ter Huurne, Fix bug in section directory creation method | |
---|---|
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 | |
16 | using namespace std; |
17 | |
18 | unique_ptr<Font> Font::defaultFont() |
19 | { |
20 | return unique_ptr<Font>(new Font(TTF_FONT, TTF_FONT_SIZE)); |
21 | } |
22 | |
23 | Font::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 | |
45 | Font::~Font() |
46 | { |
47 | if (font) { |
48 | TTF_CloseFont(font); |
49 | TTF_Quit(); |
50 | } |
51 | } |
52 | |
53 | int 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 | |
78 | string 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 | |
98 | string 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 | |
190 | int 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 | |
201 | int 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 | |
227 | int 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 |
Branches:
install_locations
master
opkrun
packages