/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "common/debug.h" #include "common/tokenizer.h" #include "common/unicode-bidi.h" #include "graphics/macgui/mactext.h" namespace Graphics { #define DEBUG 0 #if DEBUG #define D(...) debug(__VA_ARGS__) #define DN(...) debugN(__VA_ARGS__) #else #define D(...) ((void)0) #define DN(...) ((void)0) #endif MacTextCanvas::~MacTextCanvas() { delete _surface; delete _shadowSurface; for (auto &t : _text) { delete t.table; delete t.tableSurface; } } // Adds the given string to the end of the last line/chunk // while observing the _canvas._maxWidth and keeping this chunk's // formatting void MacTextCanvas::chopChunk(const Common::U32String &str, int *curLinePtr, int indent, int maxWidth) { int curLine = *curLinePtr; int curChunk; MacFontRun *chunk; curChunk = _text[curLine].chunks.size() - 1; chunk = &_text[curLine].chunks[curChunk]; // Check if there is nothing to add, then remove the last chunk // This happens when the previous run is finished only with // empty formatting, or when we were adding text for the first time if (chunk->text.empty() && str.empty() && (_text[curLine].chunks.size() > 1)) { D(9, "** chopChunk, replaced formatting, line %d", curLine); _text[curLine].chunks.pop_back(); return; } // If maxWidth is not restricted (-1 means possibly invalid width), just append and return if (maxWidth == -1) { chunk->text += str; return; } Common::Array text; Common::Array lineContinuations; int w = getLineWidth(curLine, true); D(9, "** chopChunk before wrap \"%s\"", Common::toPrintable(str.encode()).c_str()); chunk->getFont()->wordWrapText(str, maxWidth, text, lineContinuations, w); for (int i = 0; i < (int)text.size(); i++) { D(9, "Line Continuations [%d] : %d", i, lineContinuations[i]); } if (text.empty()) { D(5, "chopChunk: too narrow width, >%d", maxWidth); if (w < maxWidth) { chunk->text += str; // Only append if within bounds } getLineCharWidth(curLine, true); return; } for (int i = 0; i < (int)text.size(); i++) { D(9, "** chopChunk result %d \"%s\"", i, toPrintable(text[i].encode()).c_str()); } chunk->text += text[0]; // Ensure line continuations is valid before accesing index 0 if (!lineContinuations.empty()) { _text[curLine].wordContinuation = lineContinuations[0]; } // Recalc dims getLineWidth(curLine, true); D(9, "** chopChunk, subchunk: \"%s\" (%d lines, maxW: %d)", toPrintable(text[0].encode()).c_str(), text.size(), maxWidth); // We do not overlap, so we're done if (text.size() == 1) return; // Now add rest of the chunks MacFontRun newchunk = *chunk; for (uint i = 1; i < text.size(); i++) { newchunk.text = text[i]; curLine++; _text.insert_at(curLine, MacTextLine()); _text[curLine].chunks.push_back(newchunk); _text[curLine].indent = indent; _text[curLine].firstLineIndent = 0; _text[curLine].wordContinuation = lineContinuations[i]; D(9, "** chopChunk, added line (firstIndent: %d): \"%s\"", _text[curLine].firstLineIndent, toPrintable(text[i].encode()).c_str()); } *curLinePtr = curLine; } void MacTextCanvas::splitString(const Common::U32String &str, int curLine, MacFontRun &defaultFormatting) { D(9, "** splitString(\"%s\", %d)", toPrintable(str.encode()).c_str(), curLine); if (str.empty()) { D(9, "** splitString, empty line"); return; } (void)splitString(str.c_str(), curLine, defaultFormatting); } Common::String preprocessImageExt(const char *ptr) { // w[idth]=WWWw -- width in units 'w' // h[eight]=HHHh -- height in units 'h' // maxw[idth]=MMMm -- max-width in units 'maxw' // // units: // % for percents of the text width -> % // em for font height as a unit -> e // px for actual pixels -> p // // Translated into fixed format: // WWWWwHHHHhMMMMm -- 4 fixed hex numbers followed by units int w = 0, h = 0, maxw = 0; char wu = ' ', hu = ' ', maxwu = ' '; enum { kStateNone, kStateW, kStateH, kStateMaxW, }; int state = kStateNone; while (*ptr) { if (*ptr == ' ' || *ptr == '\t' || *ptr == ',') { ptr++; continue; } if (*ptr == '=') { ptr++; continue; } if (Common::isAlpha(*ptr)) { if (*ptr == 'w') { state = kStateW; } else if (*ptr == 'h') { state = kStateH; } else if (scumm_strnicmp(ptr, "maxw", 4) == 0) { state = kStateMaxW; } else if (*ptr == '=') { ptr++; continue; } else { warning("MacTextCanvas: Malformatted image extension: unknown key at '%s'", ptr); return ""; } while (*ptr && *ptr != '=') ptr++; if (*ptr != '=') { warning("MacTextCanvas: Malformatted image extension: '=' expected at '%s'", ptr); return ""; } } else if (Common::isDigit(*ptr)) { int num = 0; if (state == kStateNone) { warning("MacTextCanvas: Malformatted image extension: unexpected digit at '%s'", ptr); return ""; } while (*ptr && Common::isDigit(*ptr)) { num *= 10; num += *ptr - '0'; ptr++; } if (*ptr == 'e' || *ptr == '%' || *ptr == 'p') { char unit = *ptr == 'e' ? 'm' : *ptr; if (state == kStateW) { w = num; wu = unit; } else if (state == kStateH) { h = num; hu = unit; } else { maxw = num; maxwu = unit; } state = kStateNone; while (*ptr && *ptr != ' ' && *ptr != '\t' && *ptr != ',') ptr++; } else { warning("MacTextCanvas: Malformatted image extension: %% or e[m] or p[x] expected at '%s'", ptr); return ""; } } else { warning("MacTextCanvas: Malformatted image extension: w[idth], h[eight] or maxw[idth] expected at '%s'", ptr); return ""; } } return Common::String::format("%04x%c%04x%c%04x%c", w, wu, h, hu, maxw, maxwu); } const Common::U32String::value_type *MacTextCanvas::splitString(const Common::U32String::value_type *s, int curLine, MacFontRun &defaultFormatting) { if (_text.empty()) { _text.resize(1); _text[0].chunks.push_back(defaultFormatting); D(9, "** splitString, added default formatting"); } else { D(9, "** splitString, continuing, %d lines", _text.size()); } _defaultFormatting = defaultFormatting; Common::U32String tmp; if (curLine == -1 || curLine >= (int)_text.size()) curLine = _text.size() - 1; if (_text[curLine].chunks.empty()) _text[curLine].chunks.push_back(_defaultFormatting); int curChunk = _text[curLine].chunks.size() - 1; MacFontRun chunk = _text[curLine].chunks[curChunk]; int indentSize = 0; int firstLineIndent = 0; bool inTable = false; bool lineBreakOnLineEnd = false; while (*s) { firstLineIndent = 0; tmp.clear(); MacTextLine *curTextLine = &_text[curLine]; while (*s) { bool endOfLine = false; // Scan till next font change or end of line while (*s && *s != '\001') { if (*s == '\r') { s++; if (*s == '\n') { // Skip whole '\r\n' s++; if (!*s) lineBreakOnLineEnd = true; } endOfLine = true; break; } // deal with single \n if (*s == '\n') { s++; endOfLine = true; if (!*s) lineBreakOnLineEnd = true; break; } tmp += *s; s++; } if (*s == '\001') // If it was \001, skip it s++; if (*s == '\001') { // \001\001 -> \001 tmp += *s++; if (*s) // Check we reached end of line continue; } D(9, "\n** splitString, chunk: \"%s\"", Common::toPrintable(tmp.encode()).c_str()); // Okay, now we are either at the end of the line, or in the next // chunk definition. That means, that we have to store the previous chunk chopChunk(tmp, &curLine, indentSize, _maxWidth > 0 ? _maxWidth - indentSize : _maxWidth); curTextLine = &_text[curLine]; firstLineIndent = curTextLine->firstLineIndent; tmp.clear(); // If it is end of the line, we're done if (!*s) { D(9, "** splitString, end of line"); break; } // get format (sync with stripFormat() ) if (*s == '\016') { // human-readable format s++; // First two digits is slant, third digit is Header number switch (*s) { case '+': { // \016+XXYZ -- opening textSlant, H, indent<+Z> uint16 textSlant, headSize, indent; s++; s = readHex(&textSlant, s, 2); chunk.textSlant |= textSlant; // Setting the specified bit s = readHex(&headSize, s, 1); if (headSize >= 1 && headSize <= 6) { // set const float sizes[] = { 1, 2.0f, 1.41f, 1.155f, 1.0f, .894f, .816f }; chunk.fontSize = defaultFormatting.fontSize * sizes[headSize]; } s = readHex(&indent, s, 1); if (s) indentSize += indent * chunk.fontSize * 2; D(9, "** splitString+: fontId: %d, textSlant: %d, fontSize: %d, indent: %d", chunk.fontId, chunk.textSlant, chunk.fontSize, indent); break; } case '-': { // \016-XXYZ -- closing textSlant, H, indent<+Z> uint16 textSlant, headSize, indent; s++; s = readHex(&textSlant, s, 2); chunk.textSlant &= ~textSlant; // Clearing the specified bit s = readHex(&headSize, s, 1); if (headSize == 0xf) // reset chunk.fontSize = _defaultFormatting.fontSize; s = readHex(&indent, s, 1); if (s) indentSize -= indent * chunk.fontSize * 2; D(9, "** splitString-: fontId: %d, textSlant: %d, fontSize: %d, indent: %d", chunk.fontId, chunk.textSlant, chunk.fontSize, indent); break; } case '[': { // \016[RRGGBB -- setting color uint16 palinfo1, palinfo2, palinfo3; s++; s = readHex(&palinfo1, s, 4); s = readHex(&palinfo2, s, 4); s = readHex(&palinfo3, s, 4); chunk.palinfo1 = palinfo1; chunk.palinfo2 = palinfo2; chunk.palinfo3 = palinfo3; chunk.fgcolor = _wm->findBestColor(palinfo1 & 0xff, palinfo2 & 0xff, palinfo3 & 0xff); D(9, "** splitString[: %08x", chunk.fgcolor); break; } case ']': { // \016] -- setting default color s++; chunk.palinfo1 = _defaultFormatting.palinfo1; chunk.palinfo2 = _defaultFormatting.palinfo2; chunk.palinfo3 = _defaultFormatting.palinfo3; chunk.fgcolor = _defaultFormatting.fgcolor; D(9, "** splitString]: %08x", chunk.fgcolor); break; } case '*': { // \016*XXsssssss -- negative indent, XX size, sssss is the string s++; uint16 len; s = readHex(&len, s, 2); Common::U32String bullet = Common::U32String(s, len); s += len; firstLineIndent = -chunk.getFont()->getStringWidth(bullet); D(9, "** splitString*: %02x '%s' (%d)", len, bullet.encode().c_str(), firstLineIndent); break; } case 'i': { // \016iXXNNnnnnAAaaaaTTttt -- image, XX% width, // NN, nnnn -- filename len and text // AA, aaaa -- alt len and text // TT, tttt -- text (tooltip) len and text s++; uint16 len; s = readHex(&_text[curLine].picpercent, s, 2); s = readHex(&len, s, 2); _text[curLine].picfname = Common::U32String(s, len).encode(); s += len; s = readHex(&len, s, 2); _text[curLine].picalt = Common::U32String(s, len); s += len; s = readHex(&len, s, 2); _text[curLine].pictitle = Common::U32String(s, len); s += len; s = readHex(&len, s, 2); _text[curLine].picext = preprocessImageExt(Common::U32String(s, len).encode().c_str()); s += len; D(9, "** splitString[i]: %d%% fname: '%s' alt: '%s' title: '%s' ext: '%s'", _text[curLine].picpercent, _text[curLine].picfname.toString().c_str(), _text[curLine].picalt.encode().c_str(), _text[curLine].pictitle.encode().c_str(), _text[curLine].picext.encode().c_str()); break; } case 't': { // \016tXXXX -- switch to the requested font id s++; uint16 fontId; s = readHex(&fontId, s, 4); chunk.fontId = fontId == 0xffff ? _defaultFormatting.fontId : fontId; D(9, "** splitString[t]: fontId: %d", fontId); break; } case 'l': { // \016lLLllll -- link len and text s++; uint16 len; s = readHex(&len, s, 2); chunk.link = Common::U32String(s, len); s += len; D(9, "** splitString[l]: link: %s", chunk.link.c_str()); break; } case 'T': { // \016T -- table s++; char cmd = *s++; if (cmd == 'h') { // Header, beginning of the table curTextLine->table = new Common::Array(); inTable = true; D(9, "** splitString[table header]"); } else if (cmd == 'b') { // Body start D(9, "** splitString[body start]"); } else if (cmd == 'B') { // Body end inTable = false; D(9, "** splitString[body end]"); processTable(curLine, _maxWidth); continue; } else if (cmd == 'r') { // Row curTextLine->table->push_back(MacTextTableRow()); D(9, "** splitString[row]"); } else if (cmd == 'c') { // Cell start uint16 align; s = readHex(&align, s, 2); curTextLine->table->back().cells.push_back(MacTextCanvas()); MacTextCanvas *cellCanvas = &curTextLine->table->back().cells.back(); cellCanvas->_textAlignment = (TextAlign)align; cellCanvas->_wm = _wm; cellCanvas->_macText = _macText; cellCanvas->_maxWidth = -1; cellCanvas->_macFontMode = _macFontMode; cellCanvas->_tfgcolor = _tfgcolor; cellCanvas->_tbgcolor = _tbgcolor; D(9, "** splitString[cell start]: align: %d", align); D(9, "** splitString[RECURSION start]"); s = cellCanvas->splitString(s, curLine, _defaultFormatting); D(9, "** splitString[RECURSION end]"); } else if (cmd == 'C') { // Cell end D(9, "** splitString[cell end]"); return s; } else { error("MacText: Unknown table subcommand (%c)", cmd); } break; } default: { uint16 fontId, textSlant, fontSize, palinfo1, palinfo2, palinfo3; s = readHex(&fontId, s, 4); s = readHex(&textSlant, s, 2); s = readHex(&fontSize, s, 4); s = readHex(&palinfo1, s, 4); s = readHex(&palinfo2, s, 4); s = readHex(&palinfo3, s, 4); chunk.setValues(_wm, fontId, textSlant, fontSize, palinfo1, palinfo2, palinfo3); D(9, "** splitString: fontId: %d, textSlant: %d, fontSize: %d, fg: %04x (from %04x %04x %04x)", fontId, textSlant, fontSize, chunk.fgcolor, palinfo1, palinfo2, palinfo3); // So far, we enforce single font here, though in the future, font size could be altered if (!_macFontMode) chunk.font = _defaultFormatting.font; } } } D(9, "*** splitString: text[%d] indent: %d, fi: %d", curLine, indentSize, firstLineIndent); curTextLine->indent = indentSize; curTextLine->firstLineIndent = firstLineIndent; // Push new formatting curTextLine->chunks.push_back(chunk); // If we reached end of paragraph, go to outer loop if (endOfLine) break; } // We avoid adding new lines while in table. Recursive cell rendering // has this flag as false (obviously) if (inTable) continue; curTextLine->paragraphEnd = true; // if the chunks is empty, which means the line will not be rendered properly // so we add a empty string here if (curTextLine->chunks.empty()) { curTextLine->chunks.push_back(_defaultFormatting); } if (*s || lineBreakOnLineEnd) { // Add new line D(9, "** splitString: new line"); curLine++; _text.insert_at(curLine, MacTextLine()); _text[curLine].chunks.push_back(chunk); curTextLine = &_text[curLine]; curTextLine->indent = indentSize; curTextLine->firstLineIndent = firstLineIndent; } } #if DEBUG debugPrint("** splitString"); #endif return s; } void MacTextCanvas::reallocSurface() { // round to closest 10 //TODO: work out why this rounding doesn't correctly fill the entire width //int requiredH = (_text.size() + (_text.size() * 10 + 9) / 10) * lineH if (!_surface) { _surface = new ManagedSurface(_maxWidth, _textMaxHeight, _wm->_pixelformat); if (_textShadow) _shadowSurface = new ManagedSurface(_maxWidth, _textMaxHeight, _wm->_pixelformat); return; } if (_surface->w < _maxWidth || _surface->h < _textMaxHeight) { // realloc surface and copy old content ManagedSurface *n = new ManagedSurface(_maxWidth, _textMaxHeight, _wm->_pixelformat); n->clear(_tbgcolor); n->blitFrom(*_surface, Common::Point(0, 0)); delete _surface; _surface = n; // same as shadow surface if (_textShadow) { ManagedSurface *newShadowSurface = new ManagedSurface(_maxWidth, _textMaxHeight, _wm->_pixelformat); newShadowSurface->clear(_tbgcolor); newShadowSurface->blitFrom(*_shadowSurface, Common::Point(0, 0)); delete _shadowSurface; _shadowSurface = newShadowSurface; } } } void MacTextCanvas::render(int from, int to, int shadow) { int w = MIN(_maxWidth, _textMaxWidth); ManagedSurface *surface = shadow ? _shadowSurface : _surface; int myFrom = from, myTo = to + 1, delta = 1; if (_wm->_language == Common::HE_ISR) { myFrom = to; myTo = from - 1; delta = -1; } for (int i = myFrom; i != myTo; i += delta) { if (!_text[i].picfname.empty()) { const Surface *image = _imageArchive.getImageSurface(_text[i].picfname, _text[i].charwidth, _text[i].height); if (image) { int xOffset = (_text[i].width - _text[i].charwidth) / 2; surface->blitFrom(*image, Common::Point(xOffset, _text[i].y)); Common::Rect bbox(xOffset, _text[i].y, xOffset + image->w, _text[i].y + image->h); D(9, "MacTextCanvas::render: Image %d x %d bbox: %d, %d, %d, %d", image->w, image->h, bbox.left, bbox.top, bbox.right, bbox.bottom); } continue; } if (_text[i].tableSurface) { surface->blitFrom(*_text[i].tableSurface, Common::Point(0, _text[i].y)); D(9, "MacTextCanvas::render: Table %d x %d at: %d, %d", _text[i].tableSurface->w, _text[i].tableSurface->h, 0, _text[i].y); continue; } int xOffset = getAlignOffset(i) + _text[i].indent + _text[i].firstLineIndent; xOffset++; int start = 0, end = _text[i].chunks.size(); if (_wm->_language == Common::HE_ISR) { start = _text[i].chunks.size() - 1; end = -1; } int maxAscentForRow = 0; for (int j = start; j != end; j += delta) { if (_text[i].chunks[j].font->getFontAscent() > maxAscentForRow) maxAscentForRow = _text[i].chunks[j].font->getFontAscent(); } // TODO: _canvas._textMaxWidth, when -1, was not rendering ANY text. for (int j = start; j != end; j += delta) { D(9, "MacTextCanvas::render: line %d[%d] h:%d at %d,%d (%s) fontid: %d fontsize: %d on %dx%d, fgcolor: %08x bgcolor: %08x", i, j, _text[i].height, xOffset, _text[i].y, _text[i].chunks[j].text.encode().c_str(), _text[i].chunks[j].fontId, _text[i].chunks[j].fontSize, _surface->w, _surface->h, _text[i].chunks[j].fgcolor, _tbgcolor); if (_text[i].chunks[j].text.empty()) continue; int yOffset = 0; if (_text[i].chunks[j].font->getFontAscent() < maxAscentForRow) { yOffset = maxAscentForRow - _text[i].chunks[j].font->getFontAscent(); } if (_text[i].chunks[j].plainByteMode()) { Common::String str = _text[i].chunks[j].getEncodedText(); _text[i].chunks[j].getFont()->drawString(surface, str, xOffset, _text[i].y + yOffset, w, shadow ? _wm->_colorBlack : _text[i].chunks[j].fgcolor, kTextAlignLeft, 0, true); xOffset += _text[i].chunks[j].getFont()->getStringWidth(str); } else { if (_wm->_language == Common::HE_ISR) _text[i].chunks[j].getFont()->drawString(surface, convertBiDiU32String(_text[i].chunks[j].text, Common::BIDI_PAR_RTL), xOffset, _text[i].y + yOffset, w, shadow ? _wm->_colorBlack : _text[i].chunks[j].fgcolor, kTextAlignLeft, 0, true); else _text[i].chunks[j].getFont()->drawString(surface, convertBiDiU32String(_text[i].chunks[j].text), xOffset, _text[i].y + yOffset, w, shadow ? _wm->_colorBlack : _text[i].chunks[j].fgcolor, kTextAlignLeft, 0, true); xOffset += _text[i].chunks[j].getFont()->getStringWidth(_text[i].chunks[j].text); } } } } void MacTextCanvas::render(int from, int to) { if (_text.empty()) return; reallocSurface(); from = MAX(0, from); to = MIN(to, _text.size() - 1); // Clear the screen _surface->fillRect(Common::Rect(0, _text[from].y, _surface->w, _text[to].y + getLineHeight(to)), _tbgcolor); // render the shadow surface; if (_textShadow) render(from, to, _textShadow); render(from, to, 0); debugPrint("MacTextCanvas::render"); } int getStringMaxWordWidth(MacFontRun &format, const Common::U32String &str) { if (str.empty()) return 0; if (format.plainByteMode()) { Common::StringTokenizer tok(Common::convertFromU32String(str, format.getEncoding())); int maxW = 0; while (!tok.empty()) { int w = format.getFont()->getStringWidth(tok.nextToken()); maxW = MAX(maxW, w); } return maxW; } else { Common::U32StringTokenizer tok(str); int maxW = 0; while (!tok.empty()) { int w = format.getFont()->getStringWidth(tok.nextToken()); maxW = MAX(maxW, w); } return maxW; } } void MacTextCanvas::parsePicExt(const Common::U32String &ext, uint16 &wOut, uint16 &hOut, int defpercent) { const Common::U32String::value_type *s = ext.c_str(); D(9, "P: %s", ext.encode().c_str()); // wwwwWhhhhHmmmmM // 0123456789 bool useDefault = false; if (ext.size() == 15 && s[4] != ' ' && s[9] != ' ' && s[4] != s[9]) { warning("MacTextCanvas: Non-matching dimension unitss in image extension: '%s'", ext.encode().c_str()); useDefault = true; } // if it is empty or without dimensions, use default width percrent if (useDefault || ext.size() < 15 || (s[4] == ' ' && s[9] == ' ' && s[14] == ' ')) { float ratio = _maxWidth * defpercent / 100.0 / (float)wOut; wOut = wOut * ratio; hOut = hOut * ratio; return; } uint16 w; uint16 h; uint16 maxw; char maxwu; (void)readHex(&w, s, 4); (void)readHex(&h, &s[5], 4); (void)readHex(&maxw, &s[10], 4); maxwu = s[14]; D(9, "w: %d%c h: %d%c maxw: %d%c", w, s[4], h, s[9], maxw, s[14]); if (s[9] == '%') { warning("MacTextCanvas: image height in %% is not supported"); h = 0; } float ratio = 1.0; // Percent of the total width if (s[4] == '%' || s[5] == '%') { ratio = _maxWidth * w / 100.0 / (float)wOut; wOut = wOut * ratio; hOut = hOut * ratio; // Size in em (font height) units } else if (s[4] == 'm' || s[5] == 'm') { int em = _defaultFormatting.fontSize; D(9, "em: %d", em); if (w != 0 && h != 0) { wOut = em * w; hOut = em * h; } else if (w != 0) { ratio = em * w / (float)wOut; wOut = wOut * ratio; hOut = hOut * ratio; } else { ratio = em * h / (float)hOut; wOut = wOut * ratio; hOut = hOut * ratio; } // Size in pixels } else if (s[4] == 'p' || s[5] == 'p') { if (w != 0 && h != 0) { wOut = w; hOut = h; } else if (w != 0) { ratio = w / (float)wOut; wOut = wOut * ratio; hOut = hOut * ratio; } else { ratio = h / (float)hOut; wOut = wOut * ratio; hOut = hOut * ratio; } } else { error("MacTextCanvas: malformed image extension '%s", ext.encode().c_str()); } D(9, "ratio is %f", ratio); if (maxw > 0 && maxwu != ' ') { int maxWidthPixels = 0; if (maxwu == '%') { maxWidthPixels = _maxWidth * maxw / 100; } else if (maxwu == 'm') { int em = _defaultFormatting.fontSize; maxWidthPixels = em * maxw; } else if (maxwu == 'p') { maxWidthPixels = maxw; } else { warning("MacTextCanvas: unknown max width unit '%c' in image extension '%s'", maxwu, ext.encode().c_str()); } if (maxWidthPixels > 0 && wOut > maxWidthPixels) { float clampRatio = maxWidthPixels / (float)wOut; D(9, "Clamping image width from %d to %d (ratio %f)", wOut, maxWidthPixels, clampRatio); wOut = maxWidthPixels; hOut = hOut * clampRatio; } } D(9, "Final dimensions: %d x %d", wOut, hOut); } int MacTextCanvas::getLineWidth(int lineNum, bool enforce, int col) { if ((uint)lineNum >= _text.size()) return 0; MacTextLine *line = &_text[lineNum]; if (line->width != -1 && !enforce && col == -1) return line->width; if (!line->picfname.empty()) { const Surface *image = _imageArchive.getImageSurface(line->picfname); if (image) { line->width = _maxWidth; uint16 w = image->w, h = image->h; parsePicExt(line->picext, w, h, line->picpercent); line->charwidth = w; line->height = h; } else { line->width = _maxWidth; line->height = 1; line->charwidth = 1; } return line->width; } if (line->table) { line->width = _maxWidth; line->height = line->tableSurface->h; line->charwidth = _maxWidth; return line->width; } int width = line->indent + line->firstLineIndent; int height = 0; int charwidth = 0; int minWidth = 0; bool firstWord = true; for (uint i = 0; i < line->chunks.size(); i++) { if (enforce && _macFontMode) line->chunks[i].font = nullptr; if (col >= 0) { if (col >= (int)line->chunks[i].text.size()) { col -= line->chunks[i].text.size(); } else { Common::U32String tmp = line->chunks[i].text.substr(0, col); width += getStringWidth(line->chunks[i], tmp); return width; } } if (!line->chunks[i].text.empty()) { int w = getStringWidth(line->chunks[i], line->chunks[i].text); int mW = getStringMaxWordWidth(line->chunks[i], line->chunks[i].text); if (firstWord) { minWidth = mW + width; // Take indent into account firstWord = false; } else { minWidth = MAX(minWidth, mW); } width += w; charwidth += line->chunks[i].text.size(); } height = MAX(height, line->chunks[i].getFont()->getFontHeight()); } line->width = width; line->minWidth = minWidth; line->height = height; line->charwidth = charwidth; return width; } int MacTextCanvas::getLineCharWidth(int line, bool enforce) { if ((uint)line >= _text.size()) return 0; if (_text[line].charwidth != -1 && !enforce) return _text[line].charwidth; int width = 0; for (uint i = 0; i < _text[line].chunks.size(); i++) { if (!_text[line].chunks[i].text.empty()) width += _text[line].chunks[i].text.size(); } _text[line].charwidth = width; return width; } int MacTextCanvas::getLineHeight(int line) { if ((uint)line >= _text.size()) return 0; (void)getLineWidth(line); // This calculates height also return _text[line].height; } void MacTextCanvas::recalcDims() { if (_text.empty()) return; int y = 0; _textMaxWidth = 0; for (uint i = 0; i < _text.size(); i++) { _text[i].y = y; // We must calculate width first, because it enforces // the computation. Calling Height() will return cached value! _textMaxWidth = MAX(_textMaxWidth, getLineWidth(i, true)); y += MAX(getLineHeight(i), _interLinear); } _textMaxHeight = y; } int MacTextCanvas::getAlignOffset(int row) { int alignOffset = 0; if (_textAlignment == kTextAlignRight) alignOffset = MAX(0, _maxWidth - getLineWidth(row) - 1); else if (_textAlignment == kTextAlignCenter) alignOffset = (_maxWidth / 2) - (getLineWidth(row) / 2); return alignOffset; } // If adjacent chunks have same format, then skip the format definition // This happens when a long paragraph is split into several lines #define ADDFORMATTING() \ if (formatted) { \ formatting = Common::U32String(_text[i].chunks[chunk].toString()); \ if (formatting != prevformatting) { \ res += formatting; \ prevformatting = formatting; \ } \ } Common::U32String MacTextCanvas::getTextChunk(int startRow, int startCol, int endRow, int endCol, bool formatted, bool newlines) { Common::U32String res(""); if (endRow == -1) endRow = _text.size() - 1; if (endCol == -1) endCol = getLineCharWidth(endRow); if (_text.empty()) { return res; } startRow = CLIP(startRow, 0, (int)_text.size() - 1); endRow = CLIP(endRow, 0, (int)_text.size() - 1); Common::U32String formatting(""), prevformatting(""); for (int i = startRow; i <= endRow; i++) { // We requested only part of one line if (i == startRow && i == endRow) { for (uint chunk = 0; chunk < _text[i].chunks.size(); chunk++) { if (_text[i].chunks[chunk].text.empty()) { // skip empty chunks, but keep them formatted, // a text input box needs to keep the formatting even when all text is removed. ADDFORMATTING(); continue; } Common::U32String nextChunk; if (startCol <= 0) { if (endCol >= (int)_text[i].chunks[chunk].text.size()) { nextChunk = _text[i].chunks[chunk].text; } else { nextChunk = _text[i].chunks[chunk].text.substr(0, endCol); } } else if ((int)_text[i].chunks[chunk].text.size() > startCol) { nextChunk = _text[i].chunks[chunk].text.substr(startCol, endCol - startCol); } if (!nextChunk.empty()) { ADDFORMATTING(); res += nextChunk; if (debugLevelSet(5)) { debugN(5, "MacTextCanvas::getTextChunk: row %d, startCol %d, endCol %d - %s\n", i, startCol, endCol, nextChunk.encode().c_str()); } } startCol -= _text[i].chunks[chunk].text.size(); endCol -= _text[i].chunks[chunk].text.size(); if (endCol <= 0) break; } // We are at the top line and it is not completely requested } else if (i == startRow && startCol != 0) { for (uint chunk = 0; chunk < _text[i].chunks.size(); chunk++) { if (_text[i].chunks[chunk].text.empty()) // skip empty chunks continue; Common::U32String nextChunk; if (startCol <= 0) { nextChunk = _text[i].chunks[chunk].text; } else if ((int)_text[i].chunks[chunk].text.size() > startCol) { nextChunk = _text[i].chunks[chunk].text.substr(startCol); } if (!nextChunk.empty()) { ADDFORMATTING(); res += nextChunk; if (debugLevelSet(5)) { debugN(5, "MacTextCanvas::getTextChunk: (topline) row %d, startCol %d, endCol %d - %s\n", i, startCol, endCol, nextChunk.encode().c_str()); } } startCol -= _text[i].chunks[chunk].text.size(); } if (newlines && _text[i].paragraphEnd) res += '\n'; // We are at the end row, and it could be not completely requested } else if (i == endRow) { for (uint chunk = 0; chunk < _text[i].chunks.size(); chunk++) { if (_text[i].chunks[chunk].text.empty()) // skip empty chunks continue; Common::U32String nextChunk; if (endCol >= (int)_text[i].chunks[chunk].text.size()) { nextChunk = _text[i].chunks[chunk].text; } else { nextChunk = _text[i].chunks[chunk].text.substr(0, endCol); } if (!nextChunk.empty()) { ADDFORMATTING(); res += nextChunk; if (debugLevelSet(5)) { debugN(5, "MacTextCanvas::getTextChunk: (endline) row %d, startCol %d, endCol %d - %s\n", i, startCol, endCol, nextChunk.encode().c_str()); } } endCol -= _text[i].chunks[chunk].text.size(); if (endCol <= 0) break; } // We are in the middle of requested range, pass whole line } else { for (uint chunk = 0; chunk < _text[i].chunks.size(); chunk++) { if (_text[i].chunks[chunk].text.empty()) // skip empty chunks continue; ADDFORMATTING(); res += _text[i].chunks[chunk].text; if (debugLevelSet(5)) { debugN(5, "MacTextCanvas::getTextChunk: (midline) row %d, startCol %d, endCol %d - %s\n", i, startCol, endCol, _text[i].chunks[chunk].text.encode().c_str()); } } if (newlines && _text[i].paragraphEnd) res += '\n'; } } return res; } void MacTextCanvas::reshuffleParagraph(int *row, int *col, MacFontRun &defaultFormatting) { _defaultFormatting = defaultFormatting; // First, we looking for the paragraph start and end int start = *row, end = *row; // Since one previous line could be affected, compute it if (start && !_text[start - 1].paragraphEnd) start--; // Find end of the paragraph while (end < (int)_text.size() - 1 && !_text[end].paragraphEnd) end++; // Get character pos within paragraph int ppos = 0; for (int i = start; i < *row; i++) ppos += getLineCharWidth(i); ppos += *col; bool paragraphEnd = _text[end].paragraphEnd; #if DEBUG D(9, "MacTextCanvas::reshuffleParagraph: ppos: %d, start: %d, end: %d", ppos, start, end); debugPrint("MacTextCanvas::reshuffleParagraph(1)"); #endif // Assemble all chunks to chop, combining the matching ones Common::Array chunks; for (int i = start; i <= end; i++) { for (auto &ch : _text[i].chunks) { if (!chunks.size()) { chunks.push_back(ch); } else { if (chunks.back().equals(ch)) chunks.back().text += ch.text; else chunks.push_back(ch); } } if (i != end && !_text[i].wordContinuation) chunks.back().text += ' '; } #if DEBUG D(9, "Chunks: "); for (auto &ch : chunks) ch.debugPrint(); D(9, ""); #endif int curLine = start; int indent = _text[curLine].indent; int firstLineIndent = _text[curLine].firstLineIndent; // Remove paragraph from the text for (int i = start; i <= end; i++) { _text.remove_at(start); } #if DEBUG debugPrint("MacTextCanvas::reshuffleParagraph(2)"); #endif // And now read it D(9, "start %d end %d", start, end); _text.insert_at(curLine, MacTextLine()); _text[curLine].indent = indent; _text[curLine].firstLineIndent = firstLineIndent; for (auto &ch : chunks) { _text[curLine].chunks.push_back(ch); _text[curLine].chunks.back().text.clear(); // We wil add it later chopChunk(ch.text, &curLine, indent, _maxWidth); } #if DEBUG debugPrint("MacTextCanvas::reshuffleParagraph(3)"); D(9, "Chunks: "); for (auto &ch : _text[curLine].chunks) ch.debugPrint(); D(9, ""); #endif // Restore the paragraph marker _text[curLine].paragraphEnd = paragraphEnd; // Find new pos within paragraph after reshuffling *row = start; while (ppos > getLineCharWidth(*row, true)) { ppos -= getLineCharWidth(*row, true); if (*row == (int)_text.size() - 1) break; (*row)++; } *col = ppos; } void MacTextCanvas::setMaxWidth(int maxWidth, MacFontRun &defaultFormatting) { if (maxWidth == _maxWidth) return; if (maxWidth < 0) { warning("MacTextCanvas::setMaxWidth(): trying to set maxWidth to %d", maxWidth); return; } _defaultFormatting = defaultFormatting; _maxWidth = maxWidth; int row, col = 0; for (uint i = 0; i < _text.size(); i++) { row = i; if (_text[i].table) { processTable(i, maxWidth); continue; } reshuffleParagraph(&row, &col, _defaultFormatting); while (i < _text.size() - 1 && !_text[i].paragraphEnd) i++; } } void MacTextCanvas::processTable(int line, int maxWidth) { Common::Array *table = _text[line].table; uint numCols = table->front().cells.size(); uint numRows = table->size(); Common::Array maxW(numCols), maxL(numCols), colW(numCols), rowH(numRows); Common::Array flex(numCols), wrap(numCols); int width = maxWidth * 0.9; int gutter = 10; // Compute column widths, both minimal and maximal for (auto &row : *table) { int i = 0; for (auto &cell : row.cells) { int cW = 0, cL = 0; for (uint l = 0; l < cell._text.size(); l++) { (void)cell.getLineWidth(l); // calculate it cW = MAX(cW, cell._text[l].width); cL = MAX(cL, cell._text[l].minWidth); } maxW[i] = MAX(maxW[i], cW); maxL[i] = MAX(maxL[i], cL); i++; } } for (uint i = 0; i < numCols; i++) { D(8, "cell #%d: width range: %d - %d", i, maxL[i], maxW[i]); wrap[i] = (maxW[i] != maxL[i]); } int left = width - (numCols - 1) * gutter; int avg = left / numCols; int nflex = 0; // determine whether columns should be flexible and assign // width of non-flexible cells for (uint i = 0; i < numCols; i++) { flex[i] = (maxW[i] > 2 * avg); if (flex[i]) { nflex++; } else { colW[i] = maxW[i]; left -= colW[i]; } } // if there is not enough space, make columns that could // be word-wrapped flexible, too if (left < nflex * avg) { for (uint i = 0; i < numCols; i++) { if (!flex[i] && wrap[i]) { left += colW[i]; colW[i] = 0; flex[i] = true; nflex += 1; } } } // Calculate weights for flexible columns. The max width // is capped at the page width to treat columns that have to // be wrapped more or less equal int tot = 0; for (uint i = 0; i < numCols; i++) { if (flex[i]) { maxW[i] = MIN(maxW[i], width); tot += maxW[i]; } } // Now assign the actual width for flexible columns. Make // sure that it is at least as long as the longest word length for (uint i = 0; i < numCols; i++) { if (flex[i]) { colW[i] = left * maxW[i] / tot; colW[i] = MAX(colW[i], maxL[i]); left -= colW[i]; } } for (uint i = 0; i < numCols; i++) { D(8, "Table cell #%d: width: %d", i, colW[i]); } int r = 0; for (auto &row : *table) { int c = 0; rowH[r] = 0; for (auto &cell : row.cells) { cell.setMaxWidth(colW[c], _defaultFormatting); cell.recalcDims(); cell.reallocSurface(); cell._surface->clear(_tbgcolor); cell.render(0, cell._text.size()); rowH[r] = MAX(rowH[r], cell._textMaxHeight); c++; } r++; } int tW = 1, tH = 1; for (uint i = 0; i < table->size(); i++) tH += rowH[i] + gutter * 2 + 1; for (uint i = 0; i < table->front().cells.size(); i++) tW += colW[i] + gutter * 2 + 1; ManagedSurface *surf = new ManagedSurface(tW, tH, _wm->_pixelformat); _text[line].tableSurface = surf; _text[line].height = tH; _text[line].width = tW; surf->clear(_tbgcolor); surf->hLine(0, 0, tW, _tfgcolor); surf->vLine(0, 0, tH, _tfgcolor); int y = 1; for (uint i = 0; i < table->size(); i++) { y += gutter * 2 + rowH[i]; surf->hLine(0, y, tW, _tfgcolor); y++; } int x = 1; for (uint i = 0; i < table->front().cells.size(); i++) { x += gutter * 2 + colW[i]; surf->vLine(x, 0, tH, _tfgcolor); x++; } r = 0; y = 1 + gutter; for (auto &row : *table) { int c = 0; x = 1 + gutter; for (auto &cell : row.cells) { surf->blitFrom(*cell._surface, Common::Point(x, y)); x += gutter * 2 + 1 + colW[c]; c++; } y += gutter * 2 + 1 + rowH[r]; r++; } } void MacFontRun::debugPrint() { DN(8, "{%d}[%d (%d)] \"%s\" ", text.size(), fontId, textSlant, Common::toPrintable(text.encode()).c_str()); } void MacTextCanvas::debugPrint(const char *prefix) { for (uint i = 0; i < _text.size(); i++) { if (prefix) DN(8, "%s: ", prefix); DN(8, "%2d, %c %c fi: %d, i: %d ", i, _text[i].paragraphEnd ? '$' : '.', _text[i].table ? 'T' : ' ', _text[i].firstLineIndent, _text[i].indent); for (uint j = 0; j < _text[i].chunks.size(); j++) _text[i].chunks[j].debugPrint(); DN(8, "\n"); } if (prefix) DN(8, "%s: ", prefix); D(8, "[done]"); } } // End of namespace Graphics