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