1 /* ScummVM - Graphic Adventure Engine
2 *
3 * ScummVM is the legal property of its developers, whose names
4 * are too numerous to list here. Please refer to the COPYRIGHT
5 * file distributed with this source distribution.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 *
21 */
22
23 #include "graphics/font.h"
24 #include "graphics/managed_surface.h"
25
26 #include "common/array.h"
27 #include "common/util.h"
28
29 namespace Graphics {
30
getKerningOffset(uint32 left,uint32 right) const31 int Font::getKerningOffset(uint32 left, uint32 right) const {
32 return 0;
33 }
34
getBoundingBox(uint32 chr) const35 Common::Rect Font::getBoundingBox(uint32 chr) const {
36 return Common::Rect(getCharWidth(chr), getFontHeight());
37 }
38
39 namespace {
40
41 template<class StringType>
getBoundingBoxImpl(const Font & font,const StringType & str,int x,int y,int w,TextAlign align,int deltax)42 Common::Rect getBoundingBoxImpl(const Font &font, const StringType &str, int x, int y, int w, TextAlign align, int deltax) {
43 // We follow the logic of drawStringImpl here. The only exception is
44 // that we do allow an empty width to be specified here. This allows us
45 // to obtain the complete bounding box of a string.
46 const int leftX = x, rightX = w ? (x + w) : 0x7FFFFFFF;
47 int width = font.getStringWidth(str);
48
49 if (align == kTextAlignCenter)
50 x = x + (w - width)/2;
51 else if (align == kTextAlignRight)
52 x = x + w - width;
53 x += deltax;
54
55 bool first = true;
56 Common::Rect bbox;
57
58 typename StringType::unsigned_type last = 0;
59 for (typename StringType::const_iterator i = str.begin(), end = str.end(); i != end; ++i) {
60 const typename StringType::unsigned_type cur = *i;
61 x += font.getKerningOffset(last, cur);
62 last = cur;
63
64 Common::Rect charBox = font.getBoundingBox(cur);
65 if (x + charBox.right > rightX)
66 break;
67 if (x + charBox.right >= leftX) {
68 charBox.translate(x, y);
69 if (first) {
70 bbox = charBox;
71 first = false;
72 } else {
73 bbox.extend(charBox);
74 }
75 }
76
77 x += font.getCharWidth(cur);
78 }
79
80 return bbox;
81 }
82
83 template<class StringType>
getStringWidthImpl(const Font & font,const StringType & str)84 int getStringWidthImpl(const Font &font, const StringType &str) {
85 int space = 0;
86 typename StringType::unsigned_type last = 0;
87
88 for (uint i = 0; i < str.size(); ++i) {
89 const typename StringType::unsigned_type cur = str[i];
90 space += font.getCharWidth(cur) + font.getKerningOffset(last, cur);
91 last = cur;
92 }
93
94 return space;
95 }
96
97 template<class StringType>
drawStringImpl(const Font & font,Surface * dst,const StringType & str,int x,int y,int w,uint32 color,TextAlign align,int deltax)98 void drawStringImpl(const Font &font, Surface *dst, const StringType &str, int x, int y, int w, uint32 color, TextAlign align, int deltax) {
99 // The logic in getBoundingImpl is the same as we use here. In case we
100 // ever change something here we will need to change it there too.
101 assert(dst != 0);
102
103 const int leftX = x, rightX = x + w;
104 int width = font.getStringWidth(str);
105
106 if (align == kTextAlignCenter)
107 x = x + (w - width)/2;
108 else if (align == kTextAlignRight)
109 x = x + w - width;
110 x += deltax;
111
112 typename StringType::unsigned_type last = 0;
113 for (typename StringType::const_iterator i = str.begin(), end = str.end(); i != end; ++i) {
114 const typename StringType::unsigned_type cur = *i;
115 x += font.getKerningOffset(last, cur);
116 last = cur;
117
118 Common::Rect charBox = font.getBoundingBox(cur);
119 if (x + charBox.right > rightX)
120 break;
121 if (x + charBox.right >= leftX)
122 font.drawChar(dst, cur, x, y, color);
123
124 x += font.getCharWidth(cur);
125 }
126 }
127
128 template<class StringType>
129 struct WordWrapper {
130 Common::Array<StringType> &lines;
131 int actualMaxLineWidth;
132
WordWrapperGraphics::__anon9ccce5cc0111::WordWrapper133 WordWrapper(Common::Array<StringType> &l) : lines(l), actualMaxLineWidth(0) {
134 }
135
addGraphics::__anon9ccce5cc0111::WordWrapper136 void add(StringType &line, int &w) {
137 if (actualMaxLineWidth < w)
138 actualMaxLineWidth = w;
139
140 lines.push_back(line);
141
142 line.clear();
143 w = 0;
144 }
145
clearGraphics::__anon9ccce5cc0111::WordWrapper146 void clear() {
147 lines.clear();
148 actualMaxLineWidth = 0;
149 }
150 };
151
152 template<class StringType>
wordWrapTextImpl(const Font & font,const StringType & str,int maxWidth,Common::Array<StringType> & lines,int initWidth,bool evenWidthLinesModeEnabled,bool wrapOnExplicitNewLines)153 int wordWrapTextImpl(const Font &font, const StringType &str, int maxWidth, Common::Array<StringType> &lines, int initWidth, bool evenWidthLinesModeEnabled, bool wrapOnExplicitNewLines) {
154 WordWrapper<StringType> wrapper(lines);
155 StringType line;
156 StringType tmpStr;
157 int lineWidth = initWidth;
158 int tmpWidth = 0;
159 int fullTextWidthEWL = initWidth; // this replaces new line characters (if any) with single spaces - it is used in Even Width Lines mode
160
161 // The rough idea behind this algorithm is as follows:
162 // We accumulate characters into the string tmpStr. Whenever a full word
163 // has been gathered together this way, we 'commit' it to the line buffer
164 // 'line', i.e. we add tmpStr to the end of line, then clear it. Before
165 // we do that, we check whether it would cause 'line' to exceed maxWidth;
166 // in that case, we first add line to lines, then reset it.
167 //
168 // If a newline character is read, then we also add line to lines and clear it.
169 //
170 // Special care has to be taken to account for 'words' that exceed the width
171 // of a line. If we encounter such a word, we have to wrap it over multiple
172 // lines.
173
174 typename StringType::unsigned_type last = 0;
175
176 // When EvenWidthLines mode is enabled then we require an early loop over the entire string
177 // in order to get the full width of the text
178 //
179 // "Wrap On Explicit New Lines" and "Even Width Lines" modes are mutually exclusive,
180 // If both are set to true and there are new line characters in the text,
181 // then "Even Width Lines" mode is disabled.
182 //
183 if (evenWidthLinesModeEnabled) {
184 // Early loop to get the full width of the text
185 for (typename StringType::const_iterator x = str.begin(); x != str.end(); ++x) {
186 typename StringType::unsigned_type c = *x;
187
188 // Check for Windows and Mac line breaks
189 if (c == '\r') {
190 if (x != str.end() && *(x + 1) == '\n') {
191 ++x;
192 }
193 c = '\n';
194 }
195
196 if (c == '\n') {
197 if (!wrapOnExplicitNewLines) {
198 c = ' ';
199 } else {
200 evenWidthLinesModeEnabled = false;
201 break;
202 }
203 }
204
205 const int w = font.getCharWidth(c) + font.getKerningOffset(last, c);
206 last = c;
207 fullTextWidthEWL += w;
208 }
209 }
210
211 int targetTotalLinesNumberEWL = 0;
212 int targetMaxLineWidth = 0;
213 do {
214 if (evenWidthLinesModeEnabled) {
215 wrapper.clear();
216 targetTotalLinesNumberEWL += 1;
217 // We add +2 to the fullTextWidthEWL to account for possible shadow pixels
218 // We add +10 * font.getCharWidth(' ') to the quotient since we want to allow some extra margin (about an extra wprd's length)
219 // since that yields better looking results
220 targetMaxLineWidth = ((fullTextWidthEWL + 2) / targetTotalLinesNumberEWL) + 10 * font.getCharWidth(' ');
221 if (targetMaxLineWidth > maxWidth) {
222 // repeat the loop with increased targetTotalLinesNumberEWL
223 continue;
224 }
225 } else {
226 targetMaxLineWidth = maxWidth;
227 }
228
229 last = 0;
230 tmpWidth = 0;
231
232 for (typename StringType::const_iterator x = str.begin(); x != str.end(); ++x) {
233 typename StringType::unsigned_type c = *x;
234
235 // Convert Windows and Mac line breaks into plain \n
236 if (c == '\r') {
237 if (x != str.end() && *(x + 1) == '\n') {
238 ++x;
239 }
240 c = '\n';
241 }
242 // if wrapping on explicit new lines is disabled, then new line characters should be treated as a single white space char
243 if (!wrapOnExplicitNewLines && c == '\n') {
244 c = ' ';
245 }
246
247 const int currentCharWidth = font.getCharWidth(c);
248 const int w = currentCharWidth + font.getKerningOffset(last, c);
249 last = c;
250 const bool wouldExceedWidth = (lineWidth + tmpWidth + w > targetMaxLineWidth);
251
252 // If this char is a whitespace, then it represents a potential
253 // 'wrap point' where wrapping could take place. Everything that
254 // came before it can now safely be added to the line, as we know
255 // that it will not have to be wrapped.
256 if (Common::isSpace(c)) {
257 line += tmpStr;
258 lineWidth += tmpWidth;
259
260 tmpStr.clear();
261 tmpWidth = 0;
262
263 // If we encounter a line break (\n), or if the new space would
264 // cause the line to overflow: start a new line
265 if ((wrapOnExplicitNewLines && c == '\n') || wouldExceedWidth) {
266 wrapper.add(line, lineWidth);
267 continue;
268 }
269 }
270
271 // If the max line width would be exceeded by adding this char,
272 // insert a line break.
273 if (wouldExceedWidth) {
274 // Commit what we have so far, *if* we have anything.
275 // If line is empty, then we are looking at a word
276 // which exceeds the maximum line width.
277 if (lineWidth > 0) {
278 wrapper.add(line, lineWidth);
279 // Trim left side
280 while (tmpStr.size() && Common::isSpace(tmpStr[0])) {
281 tmpStr.deleteChar(0);
282 // This is not very fast, but it is the simplest way to
283 // assure we do not mess something up because of kerning.
284 tmpWidth = font.getStringWidth(tmpStr);
285 }
286
287 if (tmpStr.empty()) {
288 // If tmpStr is empty, we might have removed the space before 'c'.
289 // That means we have to recompute the kerning.
290
291 tmpWidth += currentCharWidth + font.getKerningOffset(0, c);
292 tmpStr += c;
293 continue;
294 }
295 } else {
296 wrapper.add(tmpStr, tmpWidth);
297 }
298 }
299
300 tmpWidth += w;
301 tmpStr += c;
302 }
303
304 // If some text is left over, add it as the final line
305 line += tmpStr;
306 lineWidth += tmpWidth;
307 if (lineWidth > 0) {
308 wrapper.add(line, lineWidth);
309 }
310 } while (evenWidthLinesModeEnabled
311 && (targetMaxLineWidth > maxWidth));
312 return wrapper.actualMaxLineWidth;
313 }
314
315 } // End of anonymous namespace
316
getBoundingBox(const Common::String & input,int x,int y,const int w,TextAlign align,int deltax,bool useEllipsis) const317 Common::Rect Font::getBoundingBox(const Common::String &input, int x, int y, const int w, TextAlign align, int deltax, bool useEllipsis) const {
318 // In case no width was given we cannot use ellipsis or any alignment
319 // apart from left alignment.
320 if (w == 0) {
321 if (useEllipsis) {
322 warning("Font::getBoundingBox: Requested ellipsis when no width was specified");
323 }
324
325 if (align != kTextAlignLeft) {
326 warning("Font::getBoundingBox: Requested text alignment when no width was specified");
327 }
328
329 useEllipsis = false;
330 align = kTextAlignLeft;
331 }
332
333 const Common::String str = useEllipsis ? handleEllipsis(input, w) : input;
334 return getBoundingBoxImpl(*this, str, x, y, w, align, deltax);
335 }
336
getBoundingBox(const Common::U32String & str,int x,int y,const int w,TextAlign align) const337 Common::Rect Font::getBoundingBox(const Common::U32String &str, int x, int y, const int w, TextAlign align) const {
338 // In case no width was given we cannot any alignment apart from left
339 // alignment.
340 if (w == 0) {
341 if (align != kTextAlignLeft) {
342 warning("Font::getBoundingBox: Requested text alignment when no width was specified");
343 }
344
345 align = kTextAlignLeft;
346 }
347
348 return getBoundingBoxImpl(*this, str, x, y, w, align, 0);
349 }
350
getStringWidth(const Common::String & str) const351 int Font::getStringWidth(const Common::String &str) const {
352 return getStringWidthImpl(*this, str);
353 }
354
getStringWidth(const Common::U32String & str) const355 int Font::getStringWidth(const Common::U32String &str) const {
356 return getStringWidthImpl(*this, str);
357 }
358
drawChar(ManagedSurface * dst,uint32 chr,int x,int y,uint32 color) const359 void Font::drawChar(ManagedSurface *dst, uint32 chr, int x, int y, uint32 color) const {
360 drawChar(&dst->_innerSurface, chr, x, y, color);
361
362 Common::Rect charBox = getBoundingBox(chr);
363 charBox.translate(x, y);
364 dst->addDirtyRect(charBox);
365 }
366
drawString(Surface * dst,const Common::String & str,int x,int y,int w,uint32 color,TextAlign align,int deltax,bool useEllipsis) const367 void Font::drawString(Surface *dst, const Common::String &str, int x, int y, int w, uint32 color, TextAlign align, int deltax, bool useEllipsis) const {
368 Common::String renderStr = useEllipsis ? handleEllipsis(str, w) : str;
369 drawStringImpl(*this, dst, renderStr, x, y, w, color, align, deltax);
370 }
371
drawString(Surface * dst,const Common::U32String & str,int x,int y,int w,uint32 color,TextAlign align,int deltax) const372 void Font::drawString(Surface *dst, const Common::U32String &str, int x, int y, int w, uint32 color, TextAlign align, int deltax) const {
373 drawStringImpl(*this, dst, str, x, y, w, color, align, deltax);
374 }
375
drawString(ManagedSurface * dst,const Common::String & str,int x,int y,int w,uint32 color,TextAlign align,int deltax,bool useEllipsis) const376 void Font::drawString(ManagedSurface *dst, const Common::String &str, int x, int y, int w, uint32 color, TextAlign align, int deltax, bool useEllipsis) const {
377 drawString(&dst->_innerSurface, str, x, y, w, color, align, deltax, useEllipsis);
378 if (w != 0) {
379 dst->addDirtyRect(getBoundingBox(str, x, y, w, align, deltax, useEllipsis));
380 }
381 }
382
drawString(ManagedSurface * dst,const Common::U32String & str,int x,int y,int w,uint32 color,TextAlign align,int deltax) const383 void Font::drawString(ManagedSurface *dst, const Common::U32String &str, int x, int y, int w, uint32 color, TextAlign align, int deltax) const {
384 drawString(&dst->_innerSurface, str, x, y, w, color, align, deltax);
385 if (w != 0) {
386 dst->addDirtyRect(getBoundingBox(str, x, y, w, align));
387 }
388 }
389
wordWrapText(const Common::String & str,int maxWidth,Common::Array<Common::String> & lines,int initWidth,bool evenWidthLinesModeEnabled,bool wrapOnExplicitNewLines) const390 int Font::wordWrapText(const Common::String &str, int maxWidth, Common::Array<Common::String> &lines, int initWidth, bool evenWidthLinesModeEnabled, bool wrapOnExplicitNewLines) const {
391 return wordWrapTextImpl(*this, str, maxWidth, lines, initWidth, evenWidthLinesModeEnabled, wrapOnExplicitNewLines);
392 }
393
wordWrapText(const Common::U32String & str,int maxWidth,Common::Array<Common::U32String> & lines,int initWidth,bool evenWidthLinesModeEnabled,bool wrapOnExplicitNewLines) const394 int Font::wordWrapText(const Common::U32String &str, int maxWidth, Common::Array<Common::U32String> &lines, int initWidth, bool evenWidthLinesModeEnabled, bool wrapOnExplicitNewLines) const {
395 return wordWrapTextImpl(*this, str, maxWidth, lines, initWidth, evenWidthLinesModeEnabled, wrapOnExplicitNewLines);
396 }
397
handleEllipsis(const Common::String & input,int w) const398 Common::String Font::handleEllipsis(const Common::String &input, int w) const {
399 Common::String s = input;
400 int width = getStringWidth(s);
401
402 if (width > w && s.hasSuffix("...")) {
403 // String is too wide. Check whether it ends in an ellipsis
404 // ("..."). If so, remove that and try again!
405 s.deleteLastChar();
406 s.deleteLastChar();
407 s.deleteLastChar();
408 width = getStringWidth(s);
409 }
410
411 if (width > w) {
412 Common::String str;
413
414 // String is too wide. So we shorten it "intelligently" by
415 // replacing parts of the string by an ellipsis. There are
416 // three possibilities for this: replace the start, the end, or
417 // the middle of the string. What is best really depends on the
418 // context; but unless we want to make this configurable,
419 // replacing the middle seems to be a good compromise.
420
421 const int ellipsisWidth = getStringWidth("...");
422
423 // SLOW algorithm to remove enough of the middle. But it is good enough
424 // for now.
425 const int halfWidth = (w - ellipsisWidth) / 2;
426 int w2 = 0;
427 Common::String::unsigned_type last = 0;
428 uint i = 0;
429
430 for (; i < s.size(); ++i) {
431 const Common::String::unsigned_type cur = s[i];
432 int charWidth = getCharWidth(cur) + getKerningOffset(last, cur);
433 if (w2 + charWidth > halfWidth)
434 break;
435 last = cur;
436 w2 += charWidth;
437 str += cur;
438 }
439
440 // At this point we know that the first 'i' chars are together 'w2'
441 // pixels wide. We took the first i-1, and add "..." to them.
442 str += "...";
443 last = '.';
444
445 // The original string is width wide. Of those we already skipped past
446 // w2 pixels, which means (width - w2) remain.
447 // The new str is (w2+ellipsisWidth) wide, so we can accommodate about
448 // (w - (w2+ellipsisWidth)) more pixels.
449 // Thus we skip ((width - w2) - (w - (w2+ellipsisWidth))) =
450 // (width + ellipsisWidth - w)
451 int skip = width + ellipsisWidth - w;
452 for (; i < s.size() && skip > 0; ++i) {
453 const Common::String::unsigned_type cur = s[i];
454 skip -= getCharWidth(cur) + getKerningOffset(last, cur);
455 last = cur;
456 }
457
458 // Append the remaining chars, if any
459 for (; i < s.size(); ++i) {
460 str += s[i];
461 }
462
463 return str;
464 } else {
465 return s;
466 }
467 }
468
469 } // End of namespace Graphics
470