scummvm/engines/sci/graphics/text32.cpp

720 lines
22 KiB
C++
Raw Normal View History

/* 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 2
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include "common/util.h"
#include "common/stack.h"
#include "graphics/primitives.h"
#include "sci/sci.h"
#include "sci/engine/kernel.h"
#include "sci/engine/selector.h"
#include "sci/engine/state.h"
#include "sci/graphics/cache.h"
#include "sci/graphics/celobj32.h"
#include "sci/graphics/compare.h"
#include "sci/graphics/font.h"
#include "sci/graphics/frameout.h"
#include "sci/graphics/screen.h"
#include "sci/graphics/text32.h"
namespace Sci {
int16 GfxText32::_xResolution = 0;
int16 GfxText32::_yResolution = 0;
2016-03-05 23:56:38 -06:00
GfxText32::GfxText32(SegManager *segMan, GfxCache *fonts) :
_segMan(segMan),
_cache(fonts),
// Not a typo, the original engine did not initialise height, only width
_width(0),
_text(""),
_bitmap(NULL_REG) {
_fontId = kSci32SystemFont;
_font = _cache->getFont(kSci32SystemFont);
if (_xResolution == 0) {
// initialize the statics
_xResolution = g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth;
_yResolution = g_sci->_gfxFrameout->getCurrentBuffer().scriptHeight;
}
}
reg_t GfxText32::createFontBitmap(int16 width, int16 height, const Common::Rect &rect, const Common::String &text, const uint8 foreColor, const uint8 backColor, const uint8 skipColor, const GuiResourceId fontId, const TextAlign alignment, const int16 borderColor, const bool dimmed, const bool doScaling, const bool gc) {
_borderColor = borderColor;
_text = text;
_textRect = rect;
_width = width;
_height = height;
_foreColor = foreColor;
_backColor = backColor;
_skipColor = skipColor;
_alignment = alignment;
_dimmed = dimmed;
setFont(fontId);
if (doScaling) {
int16 scriptWidth = g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth;
int16 scriptHeight = g_sci->_gfxFrameout->getCurrentBuffer().scriptHeight;
Ratio scaleX(_xResolution, scriptWidth);
Ratio scaleY(_yResolution, scriptHeight);
_width = (_width * scaleX).toInt();
_height = (_height * scaleY).toInt();
mulinc(_textRect, scaleX, scaleY);
}
// _textRect represents where text is drawn inside the
// bitmap; clipRect is the entire bitmap
Common::Rect bitmapRect(_width, _height);
if (_textRect.intersects(bitmapRect)) {
_textRect.clip(bitmapRect);
} else {
_textRect = Common::Rect();
}
_segMan->allocateBitmap(&_bitmap, _width, _height, _skipColor, 0, 0, _xResolution, _yResolution, 0, false, gc);
erase(bitmapRect, false);
if (_borderColor > -1) {
drawFrame(bitmapRect, 1, _borderColor, false);
}
drawTextBox();
return _bitmap;
}
reg_t GfxText32::createFontBitmap(const CelInfo32 &celInfo, const Common::Rect &rect, const Common::String &text, const int16 foreColor, const int16 backColor, const GuiResourceId fontId, const int16 skipColor, const int16 borderColor, const bool dimmed, const bool gc) {
_borderColor = borderColor;
_text = text;
_textRect = rect;
_foreColor = foreColor;
_dimmed = dimmed;
setFont(fontId);
int16 scriptWidth = g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth;
int16 scriptHeight = g_sci->_gfxFrameout->getCurrentBuffer().scriptHeight;
mulinc(_textRect, Ratio(_xResolution, scriptWidth), Ratio(_yResolution, scriptHeight));
CelObjView view(celInfo.resourceId, celInfo.loopNo, celInfo.celNo);
_skipColor = view._skipColor;
_width = view._width * _xResolution / view._xResolution;
_height = view._height * _yResolution / view._yResolution;
Common::Rect bitmapRect(_width, _height);
if (_textRect.intersects(bitmapRect)) {
_textRect.clip(bitmapRect);
} else {
_textRect = Common::Rect();
}
SciBitmap &bitmap = *_segMan->allocateBitmap(&_bitmap, _width, _height, _skipColor, 0, 0, _xResolution, _yResolution, 0, false, gc);
// NOTE: The engine filled the bitmap pixels with 11 here, which is silly
// because then it just erased the bitmap using the skip color. So we don't
// fill the bitmap redundantly here.
_backColor = _skipColor;
erase(bitmapRect, false);
_backColor = backColor;
view.draw(bitmap.getBuffer(), bitmapRect, Common::Point(0, 0), false, Ratio(_xResolution, view._xResolution), Ratio(_yResolution, view._yResolution));
if (_backColor != skipColor && _foreColor != skipColor) {
erase(_textRect, false);
}
if (text.size() > 0) {
if (_foreColor == skipColor) {
error("TODO: Implement transparent text");
} else {
if (borderColor != -1) {
drawFrame(bitmapRect, 1, _borderColor, false);
}
drawTextBox();
}
}
return _bitmap;
}
void GfxText32::setFont(const GuiResourceId fontId) {
// NOTE: In SCI engine this calls FontMgr::BuildFontTable and then a font
// table is built on the FontMgr directly; instead, because we already have
// font resources, this code just grabs a font out of GfxCache.
if (fontId != _fontId) {
_fontId = fontId;
_font = _cache->getFont(_fontId);
}
}
void GfxText32::drawFrame(const Common::Rect &rect, const int16 size, const uint8 color, const bool doScaling) {
Common::Rect targetRect = doScaling ? scaleRect(rect) : rect;
SciBitmap &bitmap = *_segMan->lookupBitmap(_bitmap);
byte *pixels = bitmap.getPixels() + rect.top * _width + rect.left;
// NOTE: Not fully disassembled, but this should be right
int16 rectWidth = targetRect.width();
int16 heightRemaining = targetRect.height();
int16 sidesHeight = heightRemaining - size * 2;
int16 centerWidth = rectWidth - size * 2;
int16 stride = _width - rectWidth;
for (int16 y = 0; y < size && y < heightRemaining; ++y) {
memset(pixels, color, rectWidth);
pixels += _width;
--heightRemaining;
}
for (int16 y = 0; y < sidesHeight; ++y) {
for (int16 x = 0; x < size; ++x) {
*pixels++ = color;
}
pixels += centerWidth;
for (int16 x = 0; x < size; ++x) {
*pixels++ = color;
}
pixels += stride;
}
for (int16 y = 0; y < size && y < heightRemaining; ++y) {
memset(pixels, color, rectWidth);
pixels += _width;
--heightRemaining;
}
}
2016-03-05 23:56:38 -06:00
void GfxText32::drawChar(const char charIndex) {
SciBitmap &bitmap = *_segMan->lookupBitmap(_bitmap);
byte *pixels = bitmap.getPixels();
_font->drawToBuffer((unsigned char)charIndex, _drawPosition.y, _drawPosition.x, _foreColor, _dimmed, pixels, _width, _height);
_drawPosition.x += _font->getCharWidth((unsigned char)charIndex);
}
2016-03-05 23:56:38 -06:00
uint16 GfxText32::getCharWidth(const char charIndex, const bool doScaling) const {
uint16 width = _font->getCharWidth((unsigned char)charIndex);
if (doScaling) {
width = scaleUpWidth(width);
}
return width;
}
void GfxText32::drawTextBox() {
if (_text.size() == 0) {
return;
}
const char *text = _text.c_str();
const char *sourceText = text;
int16 textRectWidth = _textRect.width();
_drawPosition.y = _textRect.top;
uint charIndex = 0;
if (g_sci->getGameId() == GID_SQ6 || g_sci->getGameId() == GID_MOTHERGOOSEHIRES) {
if (getLongest(&charIndex, textRectWidth) == 0) {
error("DrawTextBox GetLongest=0");
}
}
charIndex = 0;
uint nextCharIndex = 0;
while (*text != '\0') {
_drawPosition.x = _textRect.left;
uint length = getLongest(&nextCharIndex, textRectWidth);
int16 textWidth = getTextWidth(charIndex, length);
if (_alignment == kTextAlignCenter) {
_drawPosition.x += (textRectWidth - textWidth) / 2;
} else if (_alignment == kTextAlignRight) {
_drawPosition.x += textRectWidth - textWidth;
}
drawText(charIndex, length);
charIndex = nextCharIndex;
text = sourceText + charIndex;
_drawPosition.y += _font->getHeight();
}
}
2016-03-05 23:56:38 -06:00
void GfxText32::drawTextBox(const Common::String &text) {
_text = text;
drawTextBox();
}
void GfxText32::drawText(const uint index, uint length) {
assert(index + length <= _text.size());
// NOTE: This draw loop implementation is somewhat different than the
// implementation in the actual engine, but should be accurate. Primarily
// the changes revolve around eliminating some extra temporaries and
// fixing the logic to match.
const char *text = _text.c_str() + index;
while (length-- > 0) {
char currentChar = *text++;
if (currentChar == '|') {
const char controlChar = *text++;
--length;
if (length == 0) {
return;
}
if (controlChar == 'a' || controlChar == 'c' || controlChar == 'f') {
uint16 value = 0;
while (length > 0) {
const char valueChar = *text;
if (valueChar < '0' || valueChar > '9') {
break;
}
++text;
--length;
value = 10 * value + (valueChar - '0');
}
if (length == 0) {
return;
}
if (controlChar == 'a') {
_alignment = (TextAlign)value;
} else if (controlChar == 'c') {
_foreColor = value;
} else if (controlChar == 'f') {
setFont(value);
}
}
while (length > 0 && *text != '|') {
++text;
--length;
}
if (length > 0) {
++text;
--length;
}
} else {
drawChar(currentChar);
}
}
}
void GfxText32::invertRect(const reg_t bitmapId, int16 bitmapStride, const Common::Rect &rect, const uint8 foreColor, const uint8 backColor, const bool doScaling) {
2016-03-05 23:56:38 -06:00
Common::Rect targetRect = rect;
if (doScaling) {
bitmapStride = bitmapStride * _xResolution / g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth;
2016-03-05 23:56:38 -06:00
targetRect = scaleRect(rect);
}
SciBitmap &bitmap = *_segMan->lookupBitmap(bitmapId);
2016-03-05 23:56:38 -06:00
// NOTE: SCI code is super weird here; it seems to be trying to look at the
// entire size of the bitmap including the header, instead of just the pixel
// data size. We just look at the pixel size. This function generally is an
// odd duck since the stride dimension for a bitmap is built in to the bitmap
// header, so perhaps it was once an unheadered bitmap format and this
// function was never updated to match? Or maybe they exploit the
// configurable stride length somewhere else to do stair stepping inverts...
uint32 invertSize = targetRect.height() * bitmapStride + targetRect.width();
uint32 bitmapSize = bitmap.getDataSize();
2016-03-05 23:56:38 -06:00
if (invertSize >= bitmapSize) {
error("InvertRect too big: %u >= %u", invertSize, bitmapSize);
}
// NOTE: Actual engine just added the bitmap header size hardcoded here
byte *pixel = bitmap.getPixels() + bitmapStride * targetRect.top + targetRect.left;
2016-03-05 23:56:38 -06:00
int16 stride = bitmapStride - targetRect.width();
int16 targetHeight = targetRect.height();
int16 targetWidth = targetRect.width();
for (int16 y = 0; y < targetHeight; ++y) {
for (int16 x = 0; x < targetWidth; ++x) {
if (*pixel == foreColor) {
*pixel = backColor;
} else if (*pixel == backColor) {
*pixel = foreColor;
}
++pixel;
}
pixel += stride;
}
}
uint GfxText32::getLongest(uint *charIndex, const int16 width) {
assert(width > 0);
uint testLength = 0;
uint length = 0;
const uint initialCharIndex = *charIndex;
// The index of the next word after the last word break
uint lastWordBreakIndex = *charIndex;
const char *text = _text.c_str() + *charIndex;
char currentChar;
while ((currentChar = *text++) != '\0') {
// NOTE: In the original engine, the font, color, and alignment were
// reset here to their initial values
// The text to render contains a line break; stop at the line break
if (currentChar == '\r' || currentChar == '\n') {
// Skip the rest of the line break if it is a Windows-style
// \r\n or non-standard \n\r
// NOTE: In the original engine, the `text` pointer had not been
// advanced yet so the indexes used to access characters were
// one higher
if (
(currentChar == '\r' && text[0] == '\n') ||
(currentChar == '\n' && text[0] == '\r' && text[1] != '\n')
) {
++*charIndex;
}
// We are at the end of a line but the last word in the line made
// it too wide to fit in the text area; return up to the previous
// word
if (length && getTextWidth(initialCharIndex, testLength) > width) {
*charIndex = lastWordBreakIndex;
return length;
}
// Skip the line break and return all text seen up to now
// NOTE: In original engine, the font, color, and alignment were
// reset, then getTextWidth was called to use its side-effects to
// set font, color, and alignment according to the text from
// `initialCharIndex` to `testLength`
++*charIndex;
return testLength;
} else if (currentChar == ' ') {
// The last word in the line made it too wide to fit in the text area;
// return up to the previous word, then collapse the whitespace
// between that word and its next sibling word into the line break
if (getTextWidth(initialCharIndex, testLength) > width) {
*charIndex = lastWordBreakIndex;
const char *nextChar = _text.c_str() + lastWordBreakIndex;
while (*nextChar++ == ' ') {
++*charIndex;
}
// NOTE: In original engine, the font, color, and alignment were
// set here to the values that were seen at the last space character
return length;
}
// NOTE: In the original engine, the values of _fontId, _foreColor,
// and _alignment were stored for use in the return path mentioned
// just above here
2011-10-14 14:05:34 +03:00
// We found a word break that was within the text area, memorise it
// and continue processing. +1 on the character index because it has
// not been incremented yet so currently points to the word break
// and not the word after the break
length = testLength;
lastWordBreakIndex = *charIndex + 1;
}
// In the middle of a line, keep processing
++*charIndex;
++testLength;
// NOTE: In the original engine, the font, color, and alignment were
// reset here to their initial values
// The text to render contained no word breaks yet but is already too
// wide for the text area; just split the word in half at the point
// where it overflows
if (length == 0 && getTextWidth(initialCharIndex, testLength) > width) {
*charIndex = --testLength + lastWordBreakIndex;
return testLength;
}
}
// The complete text to render was a single word, or was narrower than
// the text area, so return the entire line
if (length == 0 || getTextWidth(initialCharIndex, testLength) <= width) {
// NOTE: In original engine, the font, color, and alignment were
// reset, then getTextWidth was called to use its side-effects to
// set font, color, and alignment according to the text from
// `initialCharIndex` to `testLength`
return testLength;
}
// The last word in the line made it wider than the text area, so return
// up to the penultimate word
*charIndex = lastWordBreakIndex;
return length;
}
int16 GfxText32::getTextWidth(const uint index, uint length) const {
int16 width = 0;
const char *text = _text.c_str() + index;
GfxFont *font = _font;
char currentChar = *text++;
while (length > 0 && currentChar != '\0') {
// Control codes are in the format `|<code><value>|`
if (currentChar == '|') {
// NOTE: Original engine code changed the global state of the
// FontMgr here upon encountering any color, alignment, or
// font control code.
// To avoid requiring all callers to manually restore these
// values on every call, we ignore control codes other than
// font change (since alignment and color do not change the
// width of characters), and simply update the font pointer
// on stack instead of the member property font.
currentChar = *text++;
--length;
if (length > 0 && currentChar == 'f') {
GuiResourceId fontId = 0;
while (length > 0 && *text >= '0' && *text <= '9') {
currentChar = *text++;
--length;
fontId = fontId * 10 + currentChar - '0';
}
if (length > 0) {
font = _cache->getFont(fontId);
}
}
// Forward through any more unknown control character data
while (length > 0 && *text != '|') {
++text;
--length;
}
if (length > 0) {
++text;
--length;
}
} else {
width += font->getCharWidth((unsigned char)currentChar);
}
if (length > 0) {
currentChar = *text++;
--length;
}
}
return width;
}
int16 GfxText32::getTextWidth(const Common::String &text, const uint index, const uint length) {
_text = text;
return scaleUpWidth(getTextWidth(index, length));
}
Common::Rect GfxText32::getTextSize(const Common::String &text, int16 maxWidth, bool doScaling) {
// NOTE: Like most of the text rendering code, this function was pretty
// weird in the original engine. The initial result rectangle was actually
// a 1x1 rectangle (0, 0, 0, 0), which was then "fixed" after the main
// text size loop finished running by subtracting 1 from the right and
// bottom edges. Like other functions in SCI32, this has been converted
// to use exclusive rects with inclusive rounding.
Common::Rect result;
int16 scriptWidth = g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth;
int16 scriptHeight = g_sci->_gfxFrameout->getCurrentBuffer().scriptHeight;
maxWidth = maxWidth * _xResolution / scriptWidth;
_text = text;
if (maxWidth >= 0) {
if (maxWidth == 0) {
maxWidth = _xResolution * 3 / 5;
}
result.right = maxWidth;
int16 textWidth = 0;
if (_text.size() > 0) {
const char *rawText = _text.c_str();
const char *sourceText = rawText;
uint charIndex = 0;
uint nextCharIndex = 0;
while (*rawText != '\0') {
uint length = getLongest(&nextCharIndex, result.width());
textWidth = MAX(textWidth, getTextWidth(charIndex, length));
charIndex = nextCharIndex;
rawText = sourceText + charIndex;
// TODO: Due to getLongest and getTextWidth not having side
// effects, it is possible that the currently loaded font's
// height is wrong for this line if it was changed inline
result.bottom += _font->getHeight();
}
}
if (textWidth < maxWidth) {
result.right = textWidth;
}
} else {
result.right = getTextWidth(0, 10000);
if (getSciVersion() < SCI_VERSION_2_1_MIDDLE) {
result.bottom = 0;
} else {
// NOTE: In the original engine code, the bottom was not decremented
// by 1, which means that the rect was actually a pixel taller than
// the height of the font. This was not the case in the other branch,
// which decremented the bottom by 1 at the end of the loop.
result.bottom = _font->getHeight() + 1;
}
}
if (doScaling) {
// NOTE: The original engine code also scaled top/left but these are
// always zero so there is no reason to do that.
result.right = ((result.right - 1) * scriptWidth + _xResolution - 1) / _xResolution + 1;
result.bottom = ((result.bottom - 1) * scriptHeight + _yResolution - 1) / _yResolution + 1;
}
return result;
}
void GfxText32::erase(const Common::Rect &rect, const bool doScaling) {
2016-03-05 23:56:38 -06:00
Common::Rect targetRect = doScaling ? scaleRect(rect) : rect;
SciBitmap &bitmap = *_segMan->lookupBitmap(_bitmap);
bitmap.getBuffer().fillRect(targetRect, _backColor);
}
int16 GfxText32::getStringWidth(const Common::String &text) {
2016-03-05 23:56:38 -06:00
return getTextWidth(text, 0, 10000);
}
int16 GfxText32::getTextCount(const Common::String &text, const uint index, const Common::Rect &textRect, const bool doScaling) {
const int16 scriptWidth = g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth;
const int16 scriptHeight = g_sci->_gfxFrameout->getCurrentBuffer().scriptHeight;
Common::Rect scaledRect(textRect);
if (doScaling) {
mulinc(scaledRect, Ratio(_xResolution, scriptWidth), Ratio(_yResolution, scriptHeight));
}
Common::String oldText = _text;
_text = text;
uint charIndex = index;
int16 maxWidth = scaledRect.width();
int16 lineCount = (scaledRect.height() - 2) / _font->getHeight();
while (lineCount--) {
getLongest(&charIndex, maxWidth);
}
_text = oldText;
return charIndex - index;
}
int16 GfxText32::getTextCount(const Common::String &text, const uint index, const GuiResourceId fontId, const Common::Rect &textRect, const bool doScaling) {
setFont(fontId);
return getTextCount(text, index, textRect, doScaling);
}
void GfxText32::scrollLine(const Common::String &lineText, int numLines, uint8 color, TextAlign align, GuiResourceId fontId, ScrollDirection dir) {
SciBitmap &bmr = *_segMan->lookupBitmap(_bitmap);
byte *pixels = bmr.getPixels();
int h = _font->getHeight();
if (dir == kScrollUp) {
// Scroll existing text down
for (int i = 0; i < (numLines - 1) * h; ++i) {
int y = _textRect.top + numLines * h - i - 1;
memcpy(pixels + y * _width + _textRect.left,
pixels + (y - h) * _width + _textRect.left,
_textRect.width());
}
} else {
// Scroll existing text up
for (int i = 0; i < (numLines - 1) * h; ++i) {
int y = _textRect.top + i;
memcpy(pixels + y * _width + _textRect.left,
pixels + (y + h) * _width + _textRect.left,
_textRect.width());
}
}
Common::Rect lineRect = _textRect;
if (dir == kScrollUp) {
lineRect.bottom = lineRect.top + h;
} else {
// It is unclear to me what the purpose of this bottom++ is.
// It does not seem to be the usual inc/exc issue.
lineRect.top += (numLines - 1) * h;
lineRect.bottom++;
}
erase(lineRect, false);
_drawPosition.x = _textRect.left;
_drawPosition.y = _textRect.top;
if (dir == kScrollDown) {
_drawPosition.y += (numLines - 1) * h;
}
_foreColor = color;
_alignment = align;
//int fc = _foreColor;
setFont(fontId);
_text = lineText;
int16 textWidth = getTextWidth(0, lineText.size());
if (_alignment == kTextAlignCenter) {
_drawPosition.x += (_textRect.width() - textWidth) / 2;
} else if (_alignment == kTextAlignRight) {
_drawPosition.x += _textRect.width() - textWidth;
}
//_foreColor = fc;
//setFont(fontId);
drawText(0, lineText.size());
}
} // End of namespace Sci