#include "BsTextData.h" #include "BsFont.h" #include "BsVector2.h" #include "BsDebug.h" namespace BansheeEngine { const int SPACE_CHAR = 32; const int TAB_CHAR = 9; void TextDataBase::TextWord::init(bool spacer) { mWidth = mHeight = 0; mSpacer = spacer; mSpaceWidth = 0; mCharsStart = 0; mCharsEnd = 0; mLastChar = nullptr; } // Assumes charIdx is an index right after last char in the list (if any). All chars need to be sequential. UINT32 TextDataBase::TextWord::addChar(UINT32 charIdx, const CHAR_DESC& desc) { UINT32 charWidth = calcCharWidth(mLastChar, desc); mWidth += charWidth; mHeight = std::max(mHeight, desc.height); if(mLastChar == nullptr) // First char mCharsStart = mCharsEnd = charIdx; else mCharsEnd = charIdx; mLastChar = &desc; return charWidth; } UINT32 TextDataBase::TextWord::calcWidthWithChar(const CHAR_DESC& desc) { return mWidth + calcCharWidth(mLastChar, desc); } UINT32 TextDataBase::TextWord::calcCharWidth(const CHAR_DESC* prevDesc, const CHAR_DESC& desc) { UINT32 charWidth = desc.xAdvance; if (prevDesc != nullptr) { UINT32 kerning = 0; for (size_t j = 0; j < prevDesc->kerningPairs.size(); j++) { if (prevDesc->kerningPairs[j].otherCharId == desc.charId) { kerning = prevDesc->kerningPairs[j].amount; break; } } charWidth += kerning; } return charWidth; } void TextDataBase::TextWord::addSpace(UINT32 spaceWidth) { mSpaceWidth += spaceWidth; mWidth = mSpaceWidth; mHeight = 0; } void TextDataBase::TextLine::init(TextDataBase* textData) { mWidth = 0; mHeight = 0; mIsEmpty = true; mTextData = textData; mWordsStart = mWordsEnd = 0; } void TextDataBase::TextLine::finalize(bool hasNewlineChar) { mHasNewline = hasNewlineChar; } void TextDataBase::TextLine::add(UINT32 charIdx, const CHAR_DESC& charDesc) { UINT32 charWidth = 0; if(mIsEmpty) { mWordsStart = mWordsEnd = MemBuffer->allocWord(false); mIsEmpty = false; } else { if(MemBuffer->WordBuffer[mWordsEnd].isSpacer()) mWordsEnd = MemBuffer->allocWord(false); } TextWord& lastWord = MemBuffer->WordBuffer[mWordsEnd]; charWidth = lastWord.addChar(charIdx, charDesc); mWidth += charWidth; mHeight = std::max(mHeight, lastWord.getHeight()); } void TextDataBase::TextLine::addSpace(UINT32 spaceWidth) { if(mIsEmpty) { mWordsStart = mWordsEnd = MemBuffer->allocWord(true); mIsEmpty = false; } else mWordsEnd = MemBuffer->allocWord(true); // Each space is counted as its own word, to make certain operations easier TextWord& lastWord = MemBuffer->WordBuffer[mWordsEnd]; lastWord.addSpace(spaceWidth); mWidth += spaceWidth; } // Assumes wordIdx is an index right after last word in the list (if any). All words need to be sequential. void TextDataBase::TextLine::addWord(UINT32 wordIdx, const TextWord& word) { if(mIsEmpty) { mWordsStart = mWordsEnd = wordIdx; mIsEmpty = false; } else mWordsEnd = wordIdx; mWidth += word.getWidth(); mHeight = std::max(mHeight, word.getHeight()); } UINT32 TextDataBase::TextLine::removeLastWord() { if(mIsEmpty) { assert(false); return 0; } UINT32 lastWord = mWordsEnd--; if(mWordsStart == lastWord) { mIsEmpty = true; mWordsStart = mWordsEnd = 0; } calculateBounds(); return lastWord; } UINT32 TextDataBase::TextLine::calcWidthWithChar(const CHAR_DESC& desc) { UINT32 charWidth = 0; UINT32 word = mWordsEnd; if (!mIsEmpty) { TextWord& lastWord = MemBuffer->WordBuffer[mWordsEnd]; if (lastWord.isSpacer()) charWidth = TextWord::calcCharWidth(nullptr, desc); else charWidth = lastWord.calcWidthWithChar(desc) - lastWord.getWidth(); } else { charWidth = TextWord::calcCharWidth(nullptr, desc); } return mWidth + charWidth; } bool TextDataBase::TextLine::isAtWordBoundary() const { return mIsEmpty || MemBuffer->WordBuffer[mWordsEnd].isSpacer(); } UINT32 TextDataBase::TextLine::fillBuffer(UINT32 page, Vector2* vertices, Vector2* uvs, UINT32* indexes, UINT32 offset, UINT32 size) const { UINT32 numQuads = 0; if(mIsEmpty) return numQuads; UINT32 penX = 0; for(UINT32 i = mWordsStart; i <= mWordsEnd; i++) { const TextWord& word = mTextData->getWord(i); if(word.isSpacer()) { // We store invisible space quads in the first page. Even though they aren't needed // for rendering and we could just leave an empty space, they are needed for intersection tests // for things like determining caret placement and selection areas if(page == 0) { INT32 curX = penX; INT32 curY = 0; UINT32 curVert = offset * 4; UINT32 curIndex = offset * 6; vertices[curVert + 0] = Vector2((float)curX, (float)curY); vertices[curVert + 1] = Vector2((float)(curX + word.getWidth()), (float)curY); vertices[curVert + 2] = Vector2((float)curX, (float)curY + (float)mTextData->getLineHeight()); vertices[curVert + 3] = Vector2((float)(curX + word.getWidth()), (float)curY + (float)mTextData->getLineHeight()); if(uvs != nullptr) { uvs[curVert + 0] = Vector2(0.0f, 0.0f); uvs[curVert + 1] = Vector2(0.0f, 0.0f); uvs[curVert + 2] = Vector2(0.0f, 0.0f); uvs[curVert + 3] = Vector2(0.0f, 0.0f); } // Triangles are back-facing which makes them invisible if(indexes != nullptr) { indexes[curIndex + 0] = curVert + 0; indexes[curIndex + 1] = curVert + 2; indexes[curIndex + 2] = curVert + 1; indexes[curIndex + 3] = curVert + 1; indexes[curIndex + 4] = curVert + 2; indexes[curIndex + 5] = curVert + 3; } offset++; numQuads++; if(offset > size) BS_EXCEPT(InternalErrorException, "Out of buffer bounds. Buffer size: " + toString(size)); } penX += word.getWidth(); } else { const CHAR_DESC& firstChar = mTextData->getChar(word.getCharsStart()); if (firstChar.xOffset < 0) penX += -firstChar.xOffset; // Offset characters so that they start at 0 UINT32 kerning = 0; for(UINT32 j = word.getCharsStart(); j <= word.getCharsEnd(); j++) { const CHAR_DESC& curChar = mTextData->getChar(j); INT32 curX = penX + curChar.xOffset; INT32 curY = ((INT32) mTextData->getBaselineOffset() - curChar.yOffset); penX += curChar.xAdvance + kerning; kerning = 0; if((j + 1) <= word.getCharsEnd()) { const CHAR_DESC& nextChar = mTextData->getChar(j + 1); for(size_t j = 0; j < curChar.kerningPairs.size(); j++) { if(curChar.kerningPairs[j].otherCharId == nextChar.charId) { kerning = curChar.kerningPairs[j].amount; break; } } } if(curChar.page != page) continue; UINT32 curVert = offset * 4; UINT32 curIndex = offset * 6; vertices[curVert + 0] = Vector2((float)curX, (float)curY); vertices[curVert + 1] = Vector2((float)(curX + curChar.width), (float)curY); vertices[curVert + 2] = Vector2((float)curX, (float)curY + (float)curChar.height); vertices[curVert + 3] = Vector2((float)(curX + curChar.width), (float)curY + (float)curChar.height); if(uvs != nullptr) { uvs[curVert + 0] = Vector2(curChar.uvX, curChar.uvY); uvs[curVert + 1] = Vector2(curChar.uvX + curChar.uvWidth, curChar.uvY); uvs[curVert + 2] = Vector2(curChar.uvX, curChar.uvY + curChar.uvHeight); uvs[curVert + 3] = Vector2(curChar.uvX + curChar.uvWidth, curChar.uvY + curChar.uvHeight); } if(indexes != nullptr) { indexes[curIndex + 0] = curVert + 0; indexes[curIndex + 1] = curVert + 1; indexes[curIndex + 2] = curVert + 2; indexes[curIndex + 3] = curVert + 1; indexes[curIndex + 4] = curVert + 3; indexes[curIndex + 5] = curVert + 2; } offset++; numQuads++; if(offset > size) BS_EXCEPT(InternalErrorException, "Out of buffer bounds. Buffer size: " + toString(size)); } } } return numQuads; } UINT32 TextDataBase::TextLine::getNumChars() const { if(mIsEmpty) return 0; UINT32 numChars = 0; for(UINT32 i = mWordsStart; i <= mWordsEnd; i++) { TextWord& word = MemBuffer->WordBuffer[i]; if(word.isSpacer()) numChars++; else numChars += (UINT32)word.getNumChars(); } return numChars; } void TextDataBase::TextLine::calculateBounds() { mWidth = 0; mHeight = 0; if(mIsEmpty) return; for(UINT32 i = mWordsStart; i <= mWordsEnd; i++) { TextWord& word = MemBuffer->WordBuffer[i]; mWidth += word.getWidth(); mHeight = std::max(mHeight, word.getHeight()); } } TextDataBase::TextDataBase(const WString& text, const HFont& font, UINT32 fontSize, UINT32 width, UINT32 height, bool wordWrap, bool wordBreak) :mFont(font), mChars(nullptr), mFontData(nullptr), mNumChars(0), mWords(nullptr), mNumWords(0), mLines(nullptr), mNumLines(0), mPageInfos(nullptr), mNumPageInfos(0) { // In order to reduce number of memory allocations algorithm first calculates data into temporary buffers and then copies the results initAlloc(); if(font != nullptr) { UINT32 nearestSize = font->getClosestSize(fontSize); mFontData = font->getBitmap(nearestSize); } if(mFontData == nullptr || mFontData->texturePages.size() == 0) return; if(mFontData->size != fontSize) { LOGWRN("Unable to find font with specified size (" + toString(fontSize) + "). Using nearest available size: " + toString(mFontData->size)); } bool widthIsLimited = width > 0; mFont = font; UINT32 curLineIdx = MemBuffer->allocLine(this); UINT32 curHeight = mFontData->fontDesc.lineHeight; UINT32 charIdx = 0; while(true) { if(charIdx >= text.size()) break; UINT32 charId = text[charIdx]; const CHAR_DESC& charDesc = mFontData->getCharDesc(charId); TextLine* curLine = &MemBuffer->LineBuffer[curLineIdx]; if(text[charIdx] == '\n' || text[charIdx] == '\r') { curLine->finalize(true); curLineIdx = MemBuffer->allocLine(this); curLine = &MemBuffer->LineBuffer[curLineIdx]; curHeight += mFontData->fontDesc.lineHeight; charIdx++; // Check for \r\n if (charIdx < text.size()) { if (text[charIdx] == '\n') charIdx++; } continue; } if (widthIsLimited && wordWrap) { UINT32 widthWithChar = 0; if (charIdx == SPACE_CHAR) widthWithChar = curLine->getWidth() + getSpaceWidth(); else if (charIdx == TAB_CHAR) widthWithChar = curLine->getWidth() + getSpaceWidth() * 4; else widthWithChar = curLine->calcWidthWithChar(charDesc); if (widthWithChar > width && !curLine->isEmpty()) { bool atWordBoundary = charId == SPACE_CHAR || charId == TAB_CHAR || curLine->isAtWordBoundary(); if (!atWordBoundary) // Need to break word into multiple pieces, or move it to next line { UINT32 lastWordIdx = curLine->removeLastWord(); TextWord& lastWord = MemBuffer->WordBuffer[lastWordIdx]; bool wordFits = lastWord.calcWidthWithChar(charDesc) <= width; if (wordFits && !curLine->isEmpty()) { curLine->finalize(false); curLineIdx = MemBuffer->allocLine(this); curLine = &MemBuffer->LineBuffer[curLineIdx]; curHeight += mFontData->fontDesc.lineHeight; curLine->addWord(lastWordIdx, lastWord); } else { if (wordBreak) { curLine->addWord(lastWordIdx, lastWord); curLine->finalize(false); curLineIdx = MemBuffer->allocLine(this); curLine = &MemBuffer->LineBuffer[curLineIdx]; curHeight += mFontData->fontDesc.lineHeight; } else { if (!curLine->isEmpty()) // Add new line unless current line is empty (to avoid constantly moving the word to new lines) { curLine->finalize(false); curLineIdx = MemBuffer->allocLine(this); curLine = &MemBuffer->LineBuffer[curLineIdx]; curHeight += mFontData->fontDesc.lineHeight; } curLine->addWord(lastWordIdx, lastWord); } } } else if (charId != SPACE_CHAR && charId != TAB_CHAR) // If current char is whitespace add it to the existing line even if it doesn't fit { curLine->finalize(false); curLineIdx = MemBuffer->allocLine(this); curLine = &MemBuffer->LineBuffer[curLineIdx]; curHeight += mFontData->fontDesc.lineHeight; } } } if(charId == SPACE_CHAR) { curLine->addSpace(getSpaceWidth()); MemBuffer->addCharToPage(0, *mFontData); } else if (charId == TAB_CHAR) { curLine->addSpace(getSpaceWidth() * 4); MemBuffer->addCharToPage(0, *mFontData); } else { curLine->add(charIdx, charDesc); MemBuffer->addCharToPage(charDesc.page, *mFontData); } charIdx++; } MemBuffer->LineBuffer[curLineIdx].finalize(true); // Now that we have all the data we need, allocate the permanent buffers and copy the data mNumChars = (UINT32)text.size(); mNumWords = MemBuffer->NextFreeWord; mNumLines = MemBuffer->NextFreeLine; mNumPageInfos = MemBuffer->NextFreePageInfo; } void TextDataBase::generatePersistentData(const WString& text, UINT8* buffer, UINT32& size, bool freeTemporary) { UINT32 charArraySize = mNumChars * sizeof(const CHAR_DESC*); UINT32 wordArraySize = mNumWords * sizeof(TextWord); UINT32 lineArraySize = mNumLines * sizeof(TextLine); UINT32 pageInfoArraySize = mNumPageInfos * sizeof(PageInfo); if (buffer == nullptr) { size = charArraySize + wordArraySize + lineArraySize + pageInfoArraySize;; return; } UINT8* dataPtr = (UINT8*)buffer; mChars = (const CHAR_DESC**)dataPtr; for (UINT32 i = 0; i < mNumChars; i++) { UINT32 charId = text[i]; const CHAR_DESC& charDesc = mFontData->getCharDesc(charId); mChars[i] = &charDesc; } dataPtr += charArraySize; mWords = (TextWord*)dataPtr; memcpy(mWords, &MemBuffer->WordBuffer[0], wordArraySize); dataPtr += wordArraySize; mLines = (TextLine*)dataPtr; memcpy(mLines, &MemBuffer->LineBuffer[0], lineArraySize); dataPtr += lineArraySize; mPageInfos = (PageInfo*)dataPtr; memcpy(mPageInfos, &MemBuffer->PageBuffer[0], pageInfoArraySize); if (freeTemporary) MemBuffer->deallocAll(); } const HTexture& TextDataBase::getTextureForPage(UINT32 page) const { return mFontData->texturePages[page]; } INT32 TextDataBase::getBaselineOffset() const { return mFontData->fontDesc.baselineOffset; } UINT32 TextDataBase::getLineHeight() const { return mFontData->fontDesc.lineHeight; } UINT32 TextDataBase::getSpaceWidth() const { return mFontData->fontDesc.spaceWidth; } void TextDataBase::initAlloc() { if (MemBuffer == nullptr) MemBuffer = bs_new(); } TextDataBase::BufferData* TextDataBase::MemBuffer = nullptr; TextDataBase::BufferData::BufferData() { WordBufferSize = 2000; LineBufferSize = 500; PageBufferSize = 20; NextFreeWord = 0; NextFreeLine = 0; NextFreePageInfo = 0; WordBuffer = bs_newN(WordBufferSize); LineBuffer = bs_newN(LineBufferSize); PageBuffer = bs_newN(PageBufferSize); } TextDataBase::BufferData::~BufferData() { bs_deleteN(WordBuffer, WordBufferSize); bs_deleteN(LineBuffer, LineBufferSize); bs_deleteN(PageBuffer, PageBufferSize); } UINT32 TextDataBase::BufferData::allocWord(bool spacer) { if(NextFreeWord >= WordBufferSize) { UINT32 newBufferSize = WordBufferSize * 2; TextWord* newBuffer = bs_newN(newBufferSize); memcpy(WordBuffer, newBuffer, WordBufferSize); bs_deleteN(WordBuffer, WordBufferSize); WordBuffer = newBuffer; WordBufferSize = newBufferSize; } WordBuffer[NextFreeWord].init(spacer); return NextFreeWord++; } UINT32 TextDataBase::BufferData::allocLine(TextDataBase* textData) { if(NextFreeLine >= LineBufferSize) { UINT32 newBufferSize = LineBufferSize * 2; TextLine* newBuffer = bs_newN(newBufferSize); memcpy(LineBuffer, newBuffer, LineBufferSize); bs_deleteN(LineBuffer, LineBufferSize); LineBuffer = newBuffer; LineBufferSize = newBufferSize; } LineBuffer[NextFreeLine].init(textData); return NextFreeLine++; } void TextDataBase::BufferData::deallocAll() { NextFreeWord = 0; NextFreeLine = 0; NextFreePageInfo = 0; } void TextDataBase::BufferData::addCharToPage(UINT32 page, const FontBitmap& fontData) { if(NextFreePageInfo >= PageBufferSize) { UINT32 newBufferSize = PageBufferSize * 2; PageInfo* newBuffer = bs_newN(newBufferSize); memcpy(PageBuffer, newBuffer, PageBufferSize); bs_deleteN(PageBuffer, PageBufferSize); PageBuffer = newBuffer; PageBufferSize = newBufferSize; } while(page >= NextFreePageInfo) { PageBuffer[NextFreePageInfo].numQuads = 0; NextFreePageInfo++; } PageBuffer[page].numQuads++; } UINT32 TextDataBase::getWidth() const { UINT32 width = 0; for(UINT32 i = 0; i < mNumLines; i++) width = std::max(width, mLines[i].getWidth()); return width; } UINT32 TextDataBase::getHeight() const { UINT32 height = 0; for(UINT32 i = 0; i < mNumLines; i++) height += mLines[i].getHeight(); return height; } }