1 /* GemRB - Infinity Engine Emulator
2 * Copyright (C) 2003 The GemRB Project
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 *
18 *
19 */
20
21 #include "Font.h"
22
23 #include "GameData.h"
24 #include "Interface.h"
25 #include "Palette.h"
26 #include "Video.h"
27
28 #include <cwctype>
29
30 namespace GemRB {
31
BlitGlyphToCanvas(const Glyph & glyph,const Point & p,ieByte * canvas,const Size & size)32 static void BlitGlyphToCanvas(const Glyph& glyph, const Point& p,
33 ieByte* canvas, const Size& size)
34 {
35 const ieByte* src = glyph.pixels;
36 if (canvas == NULL || src == NULL) {
37 return; // need both a src and dst
38 }
39
40 // find the origin and clip to it.
41 // only worry about origin < 0.
42 Point blitPoint = p + glyph.pos;
43 Size srcSize = glyph.size;
44 if (blitPoint.y < 0) {
45 int offset = (-blitPoint.y * glyph.size.w);
46 src += offset;
47 srcSize.h -= offset;
48 blitPoint.y = 0;
49 }
50 if (blitPoint.x < 0) {
51 int offset = -blitPoint.x;
52 src += offset;
53 srcSize.w -= offset;
54 blitPoint.x = 0;
55 }
56 ieByte* dest = canvas + (size.w * blitPoint.y) + blitPoint.x;
57 assert(src >= glyph.pixels);
58 assert(dest >= canvas);
59 // copy the glyph to the canvas
60 for(int row = 0; row < srcSize.h; row++ ) {
61 if (dest + srcSize.w > canvas + (size.w * size.h)) {
62 break;
63 }
64 memcpy(dest, src, srcSize.w);
65 dest += size.w;
66 src += glyph.pitch;
67 }
68 }
69
GlyphAtlasPage(Size pageSize,Font * font)70 Font::GlyphAtlasPage::GlyphAtlasPage(Size pageSize, Font* font)
71 : SpriteSheet<ieWord>(core->GetVideoDriver()), font(font)
72 {
73 pageXPos = 0;
74 SheetRegion.w = pageSize.w;
75 SheetRegion.h = pageSize.h;
76
77 pageData = (ieByte*)calloc(pageSize.h, pageSize.w);
78 }
79
AddGlyph(ieWord chr,const Glyph & g)80 bool Font::GlyphAtlasPage::AddGlyph(ieWord chr, const Glyph& g)
81 {
82 assert(glyphs.find(chr) == glyphs.end());
83 int newX = pageXPos + g.size.w;
84 if (newX > SheetRegion.w) {
85 return false;
86 }
87
88 ieByte* pixels = nullptr;
89 int glyphH = g.size.h + abs(g.pos.y);
90 if (glyphH > SheetRegion.h) {
91 // must grow to accommodate this glyph
92 if (Sheet) {
93 // if we already have a sheet we need to destroy it before we can add more glyphs
94 pageData = (ieByte*)calloc(SheetRegion.w, glyphH);
95 const ieByte* pixels = static_cast<const ieByte*>(Sheet->LockSprite());
96 std::copy(pixels, pixels + (Sheet->Frame.w * Sheet->Frame.h), pageData);
97 Sheet->UnlockSprite();
98 Sheet = nullptr;
99 } else {
100 pageData = (ieByte*)realloc(pageData, SheetRegion.w * glyphH);
101 }
102
103 assert(pageData);
104 SheetRegion.h = glyphH;
105 pixels = pageData;
106 } else if (Sheet) {
107 // we need to lock/unlock the sprite because we are updating its pixels
108 pixels = (ieByte*)Sheet->LockSprite();
109 assert(pixels == pageData);
110 } else {
111 pixels = pageData;
112 }
113
114 // have to adjust the x because BlitGlyphToCanvas will use g.pos.x, but we dont want that here.
115 BlitGlyphToCanvas(g, Point(pageXPos - g.pos.x, (g.pos.y < 0) ? -g.pos.y : 0), pageData, SheetRegion.Dimensions());
116 MapSheetSegment(chr, Region(pageXPos, (g.pos.y < 0) ? 0 : g.pos.y, g.size.w, g.size.h));
117 // make the non-temporary glyph from our own data
118 ieByte* pageLoc = pageData + pageXPos;
119 glyphs.insert(std::make_pair(chr, Glyph(g.size, g.pos, pageLoc, SheetRegion.w)));
120
121 pageXPos = newX;
122
123 if (Sheet) {
124 Sheet->UnlockSprite();
125 }
126
127 return true;
128 }
129
GlyphForChr(ieWord chr) const130 const Glyph& Font::GlyphAtlasPage::GlyphForChr(ieWord chr) const
131 {
132 GlyphMap::const_iterator it = glyphs.find(chr);
133 if (it != glyphs.end()) {
134 return it->second;
135 }
136 const static Glyph blank(Size(0,0), Point(0, 0), NULL, 0);
137 return blank;
138 }
139
Draw(ieWord chr,const Region & dest,const PrintColors * colors)140 void Font::GlyphAtlasPage::Draw(ieWord chr, const Region& dest, const PrintColors* colors)
141 {
142 // ensure that we have a sprite!
143 if (Sheet == NULL) {
144 //Sheet = core->GetVideoDriver()->CreateSprite8(SheetRegion.w, SheetRegion.h, pageData, pal, true, 0);
145 Sheet = core->GetVideoDriver()->CreateSprite8(SheetRegion, pageData, font->palette, true, 0);
146 if (font->background) {
147 invertedSheet = Sheet->copy();
148 auto invertedPalette = font->palette->Copy();
149 for (auto& c : invertedPalette->col) {
150 c.r = 255 - c.r;
151 c.g = 255 - c.g;
152 c.b = 255 - c.b;
153 }
154 invertedSheet->SetPalette(invertedPalette);
155 }
156 }
157
158 if (colors) {
159 if (font->background) {
160 SpriteSheet<ieWord>::Draw(chr, dest, BlitFlags::BLENDED | BlitFlags::COLOR_MOD, colors->bg);
161 // no point in BlitFlags::ADD with black so let's optimize away some blits
162 if (colors->fg != ColorBlack) {
163 std::swap(Sheet, invertedSheet);
164 SpriteSheet<ieWord>::Draw(chr, dest, BlitFlags::ADD | BlitFlags::COLOR_MOD, colors->fg);
165 std::swap(Sheet, invertedSheet);
166 }
167 } else {
168 SpriteSheet<ieWord>::Draw(chr, dest, BlitFlags::BLENDED | BlitFlags::COLOR_MOD, colors->fg);
169 }
170 } else {
171 SpriteSheet<ieWord>::Draw(chr, dest, BlitFlags::BLENDED, ColorWhite);
172 }
173 }
174
DumpToScreen(const Region & r)175 void Font::GlyphAtlasPage::DumpToScreen(const Region& r)
176 {
177 Video* video = core->GetVideoDriver();
178 video->SetScreenClip(NULL);
179 Region drawRgn = Region(0, 0, 1024, Sheet->Frame.h);
180 video->DrawRect(drawRgn, ColorBlack, true);
181 video->DrawRect(Sheet->Frame.Intersect(r), ColorWhite, false);
182 video->BlitSprite(Sheet, Sheet->Frame.Intersect(r), drawRgn, BlitFlags::BLENDED);
183 }
184
Font(PaletteHolder pal,ieWord lineheight,ieWord baseline,bool bg)185 Font::Font(PaletteHolder pal, ieWord lineheight, ieWord baseline, bool bg)
186 : palette(pal), LineHeight(lineheight), Baseline(baseline)
187 {
188 CurrentAtlasPage = NULL;
189 background = bg;
190 }
191
~Font(void)192 Font::~Font(void)
193 {
194 GlyphAtlas::iterator it;
195 for (it = Atlas.begin(); it != Atlas.end(); ++it) {
196 delete *it;
197 }
198 }
199
CreateGlyphIndex(ieWord chr,ieWord pageIdx,const Glyph * g)200 void Font::CreateGlyphIndex(ieWord chr, ieWord pageIdx, const Glyph* g)
201 {
202 if (chr >= AtlasIndex.size()) {
203 // potentially wasteful I guess, but much faster than a map.
204 AtlasIndex.resize(chr+1);
205 } else {
206 assert(AtlasIndex[chr].pageIdx == static_cast<ieWord>(-1));
207 }
208 AtlasIndex[chr] = GlyphIndexEntry(chr, pageIdx, g);
209 }
210
CreateGlyphForCharSprite(ieWord chr,const Holder<Sprite2D> spr)211 const Glyph& Font::CreateGlyphForCharSprite(ieWord chr, const Holder<Sprite2D> spr)
212 {
213 assert(AtlasIndex.size() <= chr || AtlasIndex[chr].pageIdx == static_cast<ieWord>(-1));
214 assert(spr);
215
216 Size size(spr->Frame.w, spr->Frame.h);
217 // FIXME: should we adjust for spr->Frame.x too?
218 Point pos(0, Baseline - spr->Frame.y);
219
220 Glyph tmp = Glyph(size, pos, (ieByte*)spr->LockSprite(), spr->Frame.w);
221 spr->UnlockSprite(); // FIXME: this is assuming it is ok to hang onto to pixel buffer returned from LockSprite()
222 // adjust the location for the glyph
223 if (!CurrentAtlasPage || !CurrentAtlasPage->AddGlyph(chr, tmp)) {
224 // page is full, make a new one
225 CurrentAtlasPage = new GlyphAtlasPage(Size(1024, LineHeight), this);
226 Atlas.push_back(CurrentAtlasPage);
227 bool ok = CurrentAtlasPage->AddGlyph(chr, tmp);
228 assert(ok);
229 }
230 assert(CurrentAtlasPage);
231 const Glyph& g = CurrentAtlasPage->GlyphForChr(chr);
232 CreateGlyphIndex(chr, Atlas.size() - 1, &g);
233 return g;
234 }
235
CreateAliasForChar(ieWord chr,ieWord alias)236 void Font::CreateAliasForChar(ieWord chr, ieWord alias)
237 {
238 // we cannot create an alias for a character that doesnt exist
239 assert(AtlasIndex.size() > chr && AtlasIndex[chr].pageIdx != static_cast<ieWord>(-1));
240
241 // we need to now find the page for the existing character and add this new one to that page
242 const GlyphIndexEntry& idx = AtlasIndex[chr]; // this referenece may become invalid after call to CreateGlyphIndex!
243 ieWord pageIdx = idx.pageIdx;
244 CreateGlyphIndex(alias, pageIdx, idx.glyph);
245 Atlas[pageIdx]->MapSheetSegment(alias, (*Atlas[pageIdx])[chr]);
246 }
247
GetGlyph(ieWord chr) const248 const Glyph& Font::GetGlyph(ieWord chr) const
249 {
250 if (chr < AtlasIndex.size()) {
251 const Glyph* g = AtlasIndex[chr].glyph;
252 if (g) {
253 return *g;
254 }
255 }
256 const static Glyph blank(Size(0,0), Point(0, 0), NULL, 0);
257 return blank;
258 }
259
RenderText(const String & string,Region & rgn,ieByte alignment,const PrintColors * colors,Point * point,ieByte ** canvas,bool grow) const260 size_t Font::RenderText(const String& string, Region& rgn, ieByte alignment, const PrintColors* colors,
261 Point* point, ieByte** canvas, bool grow) const
262 {
263 // NOTE: vertical alignment is not handled here.
264 // it should have been calculated previously and passed in via the "point" parameter
265
266 bool singleLine = (alignment&IE_FONT_SINGLE_LINE);
267 Point dp((point) ? point->x : 0, (point) ? point->y : 0);
268 const Region& sclip = core->GetVideoDriver()->GetScreenClip();
269
270 size_t charCount = 0;
271 bool lineBreak = false;
272 size_t stringPos = 0;
273 String line;
274 while (lineBreak || stringPos < string.length()) {
275 if (lineBreak) {
276 lineBreak = false;
277 } else {
278 size_t eolpos = string.find_first_of(L'\n', stringPos);
279 if (eolpos == String::npos) {
280 eolpos = string.length();
281 } else {
282 eolpos++; // convert from index
283 }
284 line = string.substr(stringPos, eolpos - stringPos);
285 stringPos = eolpos;
286 }
287
288 // check if we need to extend the canvas
289 if (canvas && grow && rgn.h < dp.y) {
290 size_t pos = (stringPos < string.length()) ? stringPos : string.length() - 1;
291 pos -= line.length();
292 Size textSize = StringSize(string.substr(pos));
293 ieWord numNewPixels = textSize.Area();
294 ieWord lineArea = rgn.w * LineHeight;
295 // round up
296 ieWord numLines = 1 + ((numNewPixels - 1) / lineArea);
297 // extend the region and canvas both
298 size_t curpos = rgn.h * rgn.w;
299 int vGrow = (numLines * LineHeight);
300 rgn.h += vGrow;
301
302 if (core->InDebugMode(ID_FONTS)) {
303 Log(MESSAGE, "Font", "Resizing canvas from %dx%d to %dx%d",
304 rgn.w, rgn.h - vGrow, rgn.w, rgn.h);
305 }
306
307 *canvas = (ieByte*)realloc(*canvas, rgn.w * rgn.h);
308 assert(canvas);
309 // fill the buffer with the color key, or the new area or we will get garbage in the areas we dont blit to
310 memset(*canvas + curpos, 0, vGrow * rgn.w);
311 }
312
313 dp.x = 0;
314 size_t lineLen = line.length();
315 if (lineLen) {
316 const Region lineRgn(dp + rgn.Origin(), Size(rgn.w, LineHeight));
317 StringSizeMetrics metrics = {lineRgn.Dimensions(), 0, 0, true};
318 const Size lineSize = StringSize(line, &metrics);
319 size_t linePos = metrics.numChars;
320 Point linePoint;
321
322 // check to see if the line is on screen
323 // TODO: technically we could be *even more* optimized by passing lineRgn, but this breaks dropcaps
324 // this isn't a big deal ATM, because the big text containers do line-by-line layout
325 if (!sclip.IntersectsRegion(rgn)) {
326 // offscreen, optimize by bypassing RenderLine, we pre-calculated linePos above
327 // alignment is completely irrelevant here since the width is the same for all alignments
328 linePoint.x = lineSize.w;
329 } else {
330 // on screen
331 if (alignment&(IE_FONT_ALIGN_CENTER|IE_FONT_ALIGN_RIGHT)) {
332 linePoint.x += (rgn.w - lineSize.w); // this is right aligned, but we can adjust for center later on
333 if (linePoint.x < 0) {
334 linePos = String::npos;
335 size_t prevPos = linePos;
336 String word;
337 while (linePoint.x < 0) {
338 // yuck, this is not optimal. not sure of a better way.
339 // we have to rewind, word by word, until X >= 0
340 linePos = line.find_last_of(L' ', prevPos);
341 if (linePos == String::npos) {
342 if (core->InDebugMode(ID_FONTS)) {
343 Log(MESSAGE, "Font", "Horizontal alignment invalidated for '%ls' due to insufficient width %d", line.c_str(), lineSize.w);
344 }
345 linePoint.x = 0;
346 break;
347 }
348 // word should be the space + word for calculation purposes
349 word = line.substr(linePos, (prevPos - linePos) + 1);
350 linePoint.x += StringSize(word).w;
351 prevPos = linePos - 1;
352 }
353 }
354 if (alignment&IE_FONT_ALIGN_CENTER) {
355 linePoint.x /= 2;
356 }
357 }
358 if (core->InDebugMode(ID_FONTS)) {
359 core->GetVideoDriver()->DrawRect(lineRgn, ColorGreen, false);
360 core->GetVideoDriver()->DrawRect(Region(linePoint + lineRgn.Origin(),
361 Size(lineSize.w, LineHeight)), ColorWhite, false);
362 }
363 linePos = RenderLine(line, lineRgn, linePoint, colors, canvas);
364 }
365 if (linePos == 0) {
366 break; // if linePos == 0 then we would loop till we are out of bounds so just stop here
367 }
368
369 dp = dp + linePoint;
370 if (linePos < line.length() - 1) {
371 // ignore whitespace between current pos and next word, if any (we are wrapping... maybe)
372 linePos = line.find_first_not_of(WHITESPACE_STRING, linePos);
373 if (linePos == String::npos) {
374 linePos = line.length() - 1; // newline char accounted for later
375 } else {
376 lineBreak = true;
377 if (!singleLine) {
378 // dont bother getting the next line if we arent going to print it
379 line = line.substr(linePos);
380 }
381 }
382 }
383 charCount += linePos;
384 }
385 dp.y += LineHeight;
386
387 if (singleLine || dp.y >= rgn.h) {
388 break;
389 }
390 }
391
392 // free the unused canvas area (if any)
393 if (canvas) {
394 int usedh = dp.y;
395 if (usedh < rgn.h) {
396 // this is more than just saving memory
397 // vertical alignment will be off if we have extra space
398 *canvas = (ieByte*)realloc(*canvas, rgn.w * usedh);
399 rgn.h = usedh;
400 }
401 }
402
403 if (point) {
404 // deal with possible trailing newline
405 if (charCount > 0 && string[charCount - 1] == L'\n') {
406 dp.y += LineHeight;
407 }
408 *point = Point(dp.x, dp.y - LineHeight);
409 }
410
411 assert(charCount <= string.length());
412 return charCount;
413 }
414
RenderLine(const String & line,const Region & lineRgn,Point & dp,const PrintColors * colors,ieByte ** canvas) const415 size_t Font::RenderLine(const String& line, const Region& lineRgn,
416 Point& dp, const PrintColors* colors, ieByte** canvas) const
417 {
418 assert(lineRgn.h == LineHeight);
419
420 // NOTE: alignment is not handled here.
421 // it should have been calculated previously and passed in via the "point" parameter
422
423 size_t linePos = 0, wordBreak = 0;
424
425 // FIXME: I'm not sure how to handle Asian text
426 // should a "word" be a single Asian glyph? that way we wouldnt clip off text (we were doing this before the rewrite too).
427 // we could check the core encoding for the 'zerospace' attribute and treat single characters as words
428 // that would looks funny with partial translations, however. we would need to handle both simultaniously.
429
430 // TODO: word breaks should probably happen on other characters such as '-' too.
431 // not as simple as adding it to find_first_of
432 bool done = false;
433 do {
434 wordBreak = line.find_first_of(L' ', linePos);
435 String word;
436 if (wordBreak == linePos) {
437 word = L' ';
438 } else {
439 word = line.substr(linePos, wordBreak - linePos);
440 }
441
442 StringSizeMetrics metrics = {lineRgn.Dimensions(), 0, 0, true};
443 int wordW = StringSize(word, &metrics).w;
444 if (dp.x == 0 && metrics.forceBreak) {
445 done = true;
446 word.resize(metrics.numChars);
447 assert(metrics.size.w <= lineRgn.w);
448 } else if (dp.x + wordW > lineRgn.w) {
449 // overflow with no wrap allowed; abort.
450 break;
451 }
452
453 // print the word
454 wchar_t currChar = '\0';
455 size_t i = 0;
456 for (; i < word.length(); i++) {
457 // process glyphs in word
458 currChar = word[i];
459 if (currChar == '\r' || currChar == '\n') {
460 continue;
461 }
462 if (i > 0) { // kerning
463 dp.x -= GetKerningOffset(word[i-1], currChar);
464 }
465
466 const Glyph& curGlyph = GetGlyph(currChar);
467 Point blitPoint = dp + lineRgn.Origin() + curGlyph.pos;
468 // use intersection because some rare glyphs can sometimes overlap lines
469 if (!lineRgn.IntersectsRegion(Region(blitPoint, curGlyph.size))) {
470 if (core->InDebugMode(ID_FONTS)) {
471 core->GetVideoDriver()->DrawRect(lineRgn, ColorRed, false);
472 }
473 assert(metrics.forceBreak == false || dp.x > 0);
474 done = true;
475 break;
476 }
477
478 if (canvas) {
479 BlitGlyphToCanvas(curGlyph, blitPoint, *canvas, lineRgn.Dimensions());
480 } else {
481 size_t pageIdx = AtlasIndex[currChar].pageIdx;
482 GlyphAtlasPage* page = Atlas[pageIdx];
483 page->Draw(currChar, Region(blitPoint, curGlyph.size), colors);
484 }
485 dp.x += curGlyph.size.w;
486 }
487 linePos += i;
488 if (done) break;
489 } while (linePos < line.length());
490 assert(linePos <= line.length());
491 return linePos;
492 }
493
RenderTextAsSprite(const String & string,const Size & size,ieByte alignment,size_t * numPrinted,Point * endPoint) const494 Holder<Sprite2D> Font::RenderTextAsSprite(const String& string, const Size& size,
495 ieByte alignment, size_t* numPrinted, Point* endPoint) const
496 {
497 Size canvasSize = StringSize(string); // same as size(0, 0)
498 // if the string is larger than the region shrink the canvas
499 // except 0 means we should size to fit in that dimension
500 if (size.w) {
501 // potentially resize
502 if (size.w < canvasSize.w) {
503 if (!(alignment&IE_FONT_SINGLE_LINE)) {
504 // we need to resize horizontally which creates new lines
505 ieWord trimmedArea = ((canvasSize.w - size.w) * canvasSize.h);
506 // this automatically becomes multiline, therefore use LineHeight
507 ieWord lineArea = size.w * LineHeight;
508 // round up
509 ieWord numLines = 1 + ((trimmedArea - 1) / lineArea);
510 if (!size.h) {
511 // grow as much as needed vertically.
512 canvasSize.h += (numLines * LineHeight);
513 // there is a chance we didn't grow enough vertically...
514 // we can't possibly know how lines will break ahead of time,
515 // over a long enough paragraph we can overflow the canvas
516 // this is handled in RenderText() by reallocing the canvas based on
517 // the same estimation algorithim (total area of text) used here
518 } else if (size.h > canvasSize.h) {
519 // grow by line increments until we hit the limit
520 // round up, because even a partial line should be blitted (and clipped)
521 ieWord maxLines = 1 + (((size.h - canvasSize.h) - 1) / LineHeight);
522 if (maxLines < numLines) {
523 numLines = maxLines;
524 }
525 canvasSize.h += (numLines * LineHeight);
526 // if the new canvas size is taller than size.h it will be dealt with later
527 }
528 }
529 canvasSize.w = size.w;
530 } else if (alignment&(IE_FONT_ALIGN_CENTER|IE_FONT_ALIGN_RIGHT)) {
531 // the size width is how we center or right align so we cant trim the canvas
532 canvasSize.w = size.w;
533 }
534 // else: we already fit in the designated area (horizontally). No need to resize.
535 }
536 if (canvasSize.h < LineHeight) {
537 // should be at least LineHeight
538 canvasSize.h = LineHeight;
539 }
540 if (size.h && size.h < canvasSize.h) {
541 // we can't unbreak lines ("\n") so at best we can clip the text.
542 canvasSize.h = size.h;
543 }
544 assert(size.h || canvasSize.h >= LineHeight);
545
546 // we must calloc because not all glyphs are equal height. set remainder to the color key
547 ieByte* canvasPx = (ieByte*)calloc(canvasSize.w, canvasSize.h);
548
549 Region rgn = Region(Point(0,0), canvasSize);
550 size_t ret = RenderText(string, rgn, alignment, nullptr, endPoint, &canvasPx, (size.h) ? false : true);
551 if (numPrinted) {
552 *numPrinted = ret;
553 }
554
555 // must ue rgn! the canvas height might be changed in RenderText()
556 if (alignment&IE_FONT_ALIGN_CENTER) {
557 rgn.x = (size.w - rgn.w) / 2;
558 } else if (alignment&IE_FONT_ALIGN_RIGHT) {
559 rgn.x = size.w - rgn.w;
560 }
561 if (alignment&IE_FONT_ALIGN_MIDDLE) {
562 rgn.y = -(size.h - rgn.h) / 2;
563 } else if (alignment&IE_FONT_ALIGN_BOTTOM) {
564 rgn.y = -(size.h - rgn.h);
565 }
566 Holder<Sprite2D> canvas = core->GetVideoDriver()->CreateSprite8(rgn, canvasPx, palette, true, 0);
567
568 return canvas;
569 }
570
Print(const Region & rgn,const String & string,ieByte alignment,Point * point) const571 size_t Font::Print(const Region& rgn, const String& string, ieByte alignment, Point* point) const
572 {
573 return Print(rgn, string, alignment, nullptr, point);
574 }
575
Print(const Region & rgn,const String & string,ieByte alignment,const PrintColors & colors,Point * point) const576 size_t Font::Print(const Region& rgn, const String& string, ieByte alignment, const PrintColors& colors, Point* point) const
577 {
578 return Print(rgn, string, alignment, &colors, point);
579 }
580
Print(Region rgn,const String & string,ieByte alignment,const PrintColors * colors,Point * point) const581 size_t Font::Print(Region rgn, const String& string, ieByte alignment, const PrintColors* colors, Point* point) const
582 {
583 if (rgn.Dimensions().IsEmpty()) return 0;
584
585 Point p = (point) ? *point : Point();
586 if (alignment&(IE_FONT_ALIGN_MIDDLE|IE_FONT_ALIGN_BOTTOM)) {
587 // we assume that point will be an offset from midde/bottom position
588 Size stringSize;
589 if (alignment&IE_FONT_SINGLE_LINE) {
590 // we can optimize single lines without StringSize()
591 stringSize.h = LineHeight;
592 } else {
593 stringSize = rgn.Dimensions();
594 StringSizeMetrics metrics = {stringSize, 0, 0, true};
595 stringSize = StringSize(string, &metrics);
596 if (alignment&IE_FONT_NO_CALC && metrics.numChars < string.length()) {
597 // PST GUISTORE, not sure what else
598 stringSize.h = rgn.h;
599 }
600 }
601
602 // important: we must do this adjustment even if it leads to -p.y!
603 // some labels depend on this behavior (BG2 GUIINV) :/
604 if (alignment&IE_FONT_ALIGN_MIDDLE) {
605 p.y += (rgn.h - stringSize.h) / 2;
606 } else { // bottom alignment
607 p.y += rgn.h - stringSize.h;
608 }
609 }
610
611 size_t ret = RenderText(string, rgn, alignment, colors, &p);
612
613 if (point) {
614 *point = p;
615 }
616 return ret;
617 }
618
StringSizeWidth(const String & string,size_t width,size_t * numChars) const619 size_t Font::StringSizeWidth(const String& string, size_t width, size_t* numChars) const
620 {
621 size_t size = 0, i = 0;
622 for (; i < string.length(); ++i) {
623 wchar_t c = string[i];
624 if (c == L'\n') {
625 break;
626 }
627
628 const Glyph& curGlyph = GetGlyph(c);
629 ieWord chrW = curGlyph.size.w;
630 if (i > 0) {
631 chrW -= GetKerningOffset(string[i-1], string[i]);
632 }
633
634 if (width > 0 && size + chrW >= width) {
635 break;
636 }
637
638 size += chrW;
639 }
640
641 if (numChars) {
642 *numChars = i;
643 }
644 return size;
645 }
646
StringSize(const String & string,StringSizeMetrics * metrics) const647 Size Font::StringSize(const String& string, StringSizeMetrics* metrics) const
648 {
649 if (!string.length()) return Size();
650 #define WILL_WRAP(val) \
651 (stop && stop->w && lineW + val > stop->w)
652
653 #define APPEND_TO_LINE(val) \
654 lineW += val; charCount = i + 1; val = 0;
655
656 ieWord w = 0, lines = 1;
657 ieWord lineW = 0, wordW = 0, spaceW = 0;
658 bool newline = false, eos = false, ws = false, forceBreak = false;
659 size_t i = 0, charCount = 0;
660 const Size* stop = (metrics) ? &metrics->size : NULL;
661 for (; i < string.length(); i++) {
662 const Glyph& curGlyph = GetGlyph(string[i]);
663 eos = (i == string.length() - 1);
664 ws = std::iswspace(string[i]);
665 if (!ws) {
666 ieWord chrW = curGlyph.size.w;
667 if (lineW > 0) { // kerning
668 chrW -= GetKerningOffset(string[i-1], string[i]);
669 }
670 if (lineW == 0 && wordW > 0 && WILL_WRAP(wordW + chrW) && metrics->forceBreak && wordW <= stop->w) {
671 // the word is longer than the line allows, but we allow a break mid-word
672 forceBreak = true;
673 newline = true;
674 APPEND_TO_LINE(wordW);
675 }
676 wordW += chrW;
677 // spaceW is the *cumulative* whitespace between the 2 words
678 wordW += spaceW;
679 spaceW = 0;
680 } // no else
681 if (ws || eos) {
682 if (WILL_WRAP(spaceW + wordW)) {
683 newline = true;
684 } else {
685 if (string[i] == L'\n') {
686 // always append *everything* if there is \n
687 lineW += spaceW; // everything else appended later
688 newline = true;
689 } else if (ws && string[i] != L'\r') {
690 spaceW += curGlyph.size.w;
691 }
692 APPEND_TO_LINE(wordW);
693 }
694 }
695
696 if (newline || eos) {
697 w = std::max(w, lineW);
698 if (stop) {
699 if (stop->h && (LineHeight * (lines + 1)) > stop->h ) {
700 break;
701 }
702 if (eos && stop->w && wordW < stop->w ) {
703 // its possible the last word of string is longer than any of the previous lines
704 w = std::max(w, wordW);
705 }
706 } else {
707 w = std::max(w, wordW);
708 }
709
710 if (metrics && metrics->numLines > 0 && metrics->numLines <= lines) {
711 break;
712 }
713
714 if (newline) {
715 newline = false;
716 lines++;
717 lineW = 0;
718 spaceW = 0;
719 }
720 }
721 }
722 #undef WILL_WRAP
723 #undef APPEND_TO_LINE
724
725 w += ((w == 0 && wordW == 0) || !newline) ? spaceW : 0;
726
727 if (metrics) {
728 if (forceBreak) charCount--;
729 metrics->forceBreak = forceBreak;
730 metrics->numChars = charCount;
731 metrics->size = Size(w, (LineHeight * lines));
732 metrics->numLines = lines;
733 if (core->InDebugMode(ID_FONTS)) {
734 assert(metrics->numChars <= string.length());
735 assert(w <= stop->w);
736 }
737 return metrics->size;
738 }
739
740 return Size(w, (LineHeight * lines));
741 }
742
743 }
744