scummvm/engines/sci/graphics/text32.cpp
Colin Snover c7c5f28bdb SCI32: Clean up scriptWidth/scriptHeight/screenWidth/screenHeight
This removes the unnecessary Buffer subclass and stops most places
where the output buffer was being interrogated about dimensions
instead of GfxFrameout.
2017-10-06 22:56:26 -05:00

737 lines
22 KiB
C++

/* 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;
GfxText32::GfxText32(SegManager *segMan, GfxCache *fonts) :
_segMan(segMan),
_cache(fonts),
// SSCI did not initialise height, so we intentionally do not do so also
_width(0),
_text(""),
_bitmap(NULL_REG) {
_fontId = kSci32SystemFont;
_font = _cache->getFont(kSci32SystemFont);
}
void GfxText32::init() {
_xResolution = g_sci->_gfxFrameout->getScriptWidth();
_yResolution = g_sci->_gfxFrameout->getScriptHeight();
}
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->getScriptWidth();
int16 scriptHeight = g_sci->_gfxFrameout->getScriptHeight();
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->getScriptWidth();
int16 scriptHeight = g_sci->_gfxFrameout->getScriptHeight();
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);
// SSCI 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) {
// In SSCI, this calls FontMgr::BuildFontTable, and then a font table is
// built on the FontMgr directly; instead, because we already have GfxFont
// resources from SCI16 and those resources did not change in SCI32, this
// code just grabs those 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;
// Not fully disassembled, but appears to be correct in all cases
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;
}
}
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);
}
int16 GfxText32::getScaledFontHeight() const {
const int16 scriptHeight = g_sci->_gfxFrameout->getScriptHeight();
return (_font->getHeight() * scriptHeight + _yResolution - 1) / _yResolution;
}
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();
}
}
void GfxText32::drawTextBox(const Common::String &text) {
_text = text;
drawTextBox();
}
void GfxText32::drawText(const uint index, uint length) {
assert(index + length <= _text.size());
// This draw loop implementation is somewhat different than the
// implementation in SSCI, but is 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) {
Common::Rect targetRect = rect;
if (doScaling) {
bitmapStride = bitmapStride * _xResolution / g_sci->_gfxFrameout->getScriptWidth();
targetRect = scaleRect(rect);
}
SciBitmap &bitmap = *_segMan->lookupBitmap(bitmapId);
// SSCI 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();
if (invertSize >= bitmapSize) {
error("InvertRect too big: %u >= %u", invertSize, bitmapSize);
}
// SSCI just added a hardcoded bitmap header size here
byte *pixel = bitmap.getPixels() + bitmapStride * targetRect.top + targetRect.left;
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') {
// In SSCI, the font, color, and alignment were reset here to their
// initial values; this does not seem to be necessary and really
// complicates the font system, so we do not do it
// 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)
// In SSCI, the `text` pointer had not been advanced yet here, so
// the indexes used to access characters were one higher there
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
// In SSCI, 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`. This is complicated and
// apparently not necessary for correct operation, so we do not do
// it
++*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;
}
// In SSCI, the font, color, and alignment were set here to the
// values that were seen at the last space character, but this
// is complicated and unnecessary so we do not do it
return length;
}
// In SSCI, the values of `_fontId`, `_foreColor`, and `_alignment`
// were stored for use in the return path mentioned just above here,
// but we do not need to do this because we do not cause
// side-effects when calculating text dimensions
// 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;
// In SSCI, the font, color, and alignment were reset here to their
// initial values, but we do not need to do this because we do not cause
// side-effects when calculating text dimensions
// 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) {
// In SSCI, 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`. This
// is not necessary because we do not cause side-effects when
// calculating text dimensions
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 == '|') {
// SSCI 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 to avoid these unnecessary side-effects.
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) {
// Like most of the text rendering code, this function was pretty weird in
// SSCI. 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->getScriptWidth();
int16 scriptHeight = g_sci->_gfxFrameout->getScriptHeight();
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 {
// In SSCI, 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. For accuracy, we do what SSCI
// did, even though this means the result is a pixel off
result.bottom = _font->getHeight() + 1;
}
}
if (doScaling) {
// SSCI 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) {
Common::Rect targetRect = doScaling ? scaleRect(rect) : rect;
SciBitmap &bitmap = *_segMan->lookupBitmap(_bitmap);
bitmap.getBuffer().fillRect(targetRect, _backColor);
}
int16 GfxText32::getStringWidth(const Common::String &text) {
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->getScriptWidth();
const int16 scriptHeight = g_sci->_gfxFrameout->getScriptHeight();
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;
// As with other font functions, SSCI saved _foreColor here so it could be
// restored after the getTextWidth call, but this call is side-effect-free
// in our implementation so this is not necessary
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 and font were restored here in SSCI due to side-effects of
// getTextWidth which do not exist in our implementation
drawText(0, lineText.size());
}
} // End of namespace Sci