Browse Source

Added TextUtility - a better way of generating renderable text

Marko Pintera 12 years ago
parent
commit
80136c13e9

+ 1 - 0
BansheeEngine/Include/BsGUIElement.h

@@ -86,6 +86,7 @@ namespace BansheeEngine
 		//	onKeyReleased
 	protected:
 		friend class GUIWidget;
+		friend class GUILayout;
 		friend class GUILayoutX;
 		friend class GUILayoutY;
 

+ 2 - 0
CamelotCore/CamelotCore.vcxproj

@@ -233,6 +233,7 @@
     <ClInclude Include="Include\CmPixelBuffer.h" />
     <ClInclude Include="Include\CmGpuProgIncludeImporter.h" />
     <ClInclude Include="Include\CmTextureView.h" />
+    <ClInclude Include="Include\CmTextUtility.h" />
     <ClInclude Include="Include\CmVertexBuffer.h" />
     <ClInclude Include="Include\CmHighLevelGpuProgram.h" />
     <ClInclude Include="Include\CmHighLevelGpuProgramManager.h" />
@@ -335,6 +336,7 @@
     <ClCompile Include="Source\CmPixelData.cpp" />
     <ClCompile Include="Source\CmPixelUtil.cpp" />
     <ClCompile Include="Source\CmTextureView.cpp" />
+    <ClCompile Include="Source\CmTextUtility.cpp" />
     <ClCompile Include="Source\CmVertexBuffer.cpp" />
     <ClCompile Include="Source\CmHighLevelGpuProgram.cpp" />
     <ClCompile Include="Source\CmHighLevelGpuProgramManager.cpp" />

+ 6 - 0
CamelotCore/CamelotCore.vcxproj.filters

@@ -441,6 +441,9 @@
     <ClInclude Include="Include\CmCursor.h">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="Include\CmTextUtility.h">
+      <Filter>Header Files\Text</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="Source\CmApplication.cpp">
@@ -677,5 +680,8 @@
     <ClCompile Include="Source\Win32\CmCursorImpl.cpp">
       <Filter>Source Files\Win32</Filter>
     </ClCompile>
+    <ClCompile Include="Source\CmTextUtility.cpp">
+      <Filter>Source Files\Text</Filter>
+    </ClCompile>
   </ItemGroup>
 </Project>

+ 103 - 0
CamelotCore/Include/CmTextUtility.h

@@ -0,0 +1,103 @@
+#pragma once
+
+#include "CmPrerequisites.h"
+#include "CmFontDesc.h"
+#include "CmInt2.h"
+
+namespace CamelotFramework
+{
+	class CM_EXPORT TextUtility
+	{
+	private:
+		class TextWord
+		{
+		public:
+			TextWord(bool spacer);
+
+			void addChar(const CHAR_DESC& desc);
+			void addSpace(UINT32 spaceWidth);
+
+			void removeLastChar();
+
+			UINT32 getWidth() const { return mWidth; }
+			UINT32 getHeight() const { return mHeight; }
+			bool isSpacer() const { return mSpacer; }
+
+			const vector<CHAR_DESC>::type& getChars() const { return mChars; }
+
+		private:
+			vector<CHAR_DESC>::type mChars;
+			UINT32 mWidth;
+			UINT32 mHeight;
+			bool mSpacer;
+			UINT32 mSpaceWidth;
+
+			void calculateWidth();
+		};
+
+	public:
+		class TextLine
+		{
+		public:
+			TextLine();
+			~TextLine();
+
+			UINT32 getWidth() const { return mWidth; }
+			UINT32 getHeight() const { return mHeight; }
+
+			/**
+			 * @brief	Gets a number quads used by all characters for every page used by this text line.
+			 *
+			 * @note	One page generally corresponds to one bitmap from which the characters are read from.
+			 * 			
+			 *			One character is represented with a single quad. Some pages might be empty.
+			 */
+			std::vector<UINT32> getNumQuadsPerPage() const;
+
+			/**
+			 * @brief	Fills the vertex/uv/index buffers for the specified page, with all the character data needed for rendering.
+			 *
+			 * @param	page				The page. 
+			 * @param [out]	vertices		Pre-allocated array where character vertices will be written.
+			 * @param [out]	uvs				Pre-allocated array where character uv coordinates will be written.
+			 * @param [out]	indexes 		Pre-allocated array where character indices will be written.
+			 * @param	offset				Offsets the location at which the method writes to the buffers. Counted as number of quads.
+			 * @param	size				Total number of quads that can fit into the specified buffers.
+			 * @param	fontData			Information describing the font.
+			 */
+			void fillBuffer(UINT32 page, Vector2* vertices, Vector2* uvs, UINT32* indexes, UINT32 offset, UINT32 size, const FontData& fontData) const;
+
+		private:
+			friend class TextUtility;
+
+			UINT32 mWidth;
+			UINT32 mHeight;
+			vector<TextWord*>::type mWords;
+			TextWord* mLastWord;
+
+			void add(const CHAR_DESC& charDesc);
+			void addSpace(UINT32 spaceWidth);
+			void addWord(TextWord* word);
+
+			TextWord* removeLastWord();
+
+			void calculateBounds();
+		};
+
+		class TextData
+		{
+		public:
+			~TextData();
+
+			const std::vector<TextLine*>& getLines() const { return mLines; }
+			const std::vector<UINT32>& getNumQuadsPerPage() const  { return mQuadsPerPage; }
+		private:
+			friend class TextUtility;
+
+			std::vector<UINT32> mQuadsPerPage;
+			std::vector<TextLine*> mLines;
+		};
+
+		std::shared_ptr<TextUtility::TextData> getTextData(const String& text, const HFont& font, UINT32 fontSize, UINT32 width = 0, UINT32 height = 0, bool wordWrap = false);
+	};
+}

+ 357 - 0
CamelotCore/Source/CmTextUtility.cpp

@@ -0,0 +1,357 @@
+#include "CmTextUtility.h"
+#include "CmFont.h"
+#include "CmVector2.h"
+#include "CmDebug.h"
+
+namespace CamelotFramework
+{
+	const int SPACE_CHAR = 32;
+
+	TextUtility::TextWord::TextWord(bool spacer)
+		:mWidth(0), mHeight(0), mSpacer(spacer), mSpaceWidth(0)
+	{ }
+
+	void TextUtility::TextWord::addChar(const CHAR_DESC& desc)
+	{
+		mChars.push_back(desc);
+
+		calculateWidth();
+	}
+
+	void TextUtility::TextWord::addSpace(UINT32 spaceWidth)
+	{
+		mSpaceWidth += spaceWidth;
+
+		calculateWidth();
+	}
+
+	void TextUtility::TextWord::removeLastChar()
+	{
+		if(mChars.size() > 0)
+		{
+			mChars.erase(mChars.end() - 1);
+			calculateWidth();
+		}
+	}
+
+	void TextUtility::TextWord::calculateWidth()
+	{
+		if(isSpacer())
+		{
+			mWidth = mSpaceWidth;
+			mHeight = 0;
+			return;
+		}
+
+		if(mChars.size() == 0)
+		{
+			mWidth = 0;
+			mHeight = 0;
+			return;
+		}
+
+		mWidth = 0;
+		mHeight = 0;
+		UINT32 kerning = 0;
+		for(size_t i = 0; i < mChars.size() - 1; i++)
+		{
+			mWidth += mChars[i].xAdvance + kerning;
+			mHeight = std::max(mHeight, mChars[i].height);
+
+			kerning = 0;
+			for(size_t j = 0; j < mChars[i].kerningPairs.size(); j++)
+			{
+				if(mChars[i].kerningPairs[j].otherCharId == mChars[i + 1].charId)
+				{
+					kerning = mChars[i].kerningPairs[j].amount;
+					break;
+				}
+			}
+		}
+
+		mWidth += mChars[mChars.size() - 1].xAdvance + kerning;
+	}
+
+	TextUtility::TextLine::TextLine()
+		:mWidth(0), mHeight(0), mLastWord(nullptr)
+	{
+
+	}
+
+	TextUtility::TextLine::~TextLine()
+	{
+		for(auto iter = mWords.begin(); iter != mWords.end(); ++iter)
+			CM_DELETE(*iter, TextWord, ScratchAlloc);
+	}
+
+	void TextUtility::TextLine::add(const CHAR_DESC& charDesc)
+	{
+		if(mLastWord == nullptr)
+		{
+			TextWord* newWord = CM_NEW(TextWord, ScratchAlloc) TextWord(false);
+			mLastWord = newWord;
+
+			mWords.push_back(mLastWord);
+
+			mLastWord->addChar(charDesc);
+		}
+		else
+		{
+			if(mLastWord->isSpacer())
+			{
+				TextWord* newWord = CM_NEW(TextWord, ScratchAlloc) TextWord(false);
+				mLastWord = newWord;
+
+				mWords.push_back(mLastWord);
+			}
+
+			mLastWord->addChar(charDesc);
+		}
+
+		calculateBounds();
+	}
+
+	void TextUtility::TextLine::addSpace(UINT32 spaceWidth)
+	{
+		if(mLastWord == nullptr)
+		{
+			TextWord* newWord = CM_NEW(TextWord, ScratchAlloc) TextWord(true);
+			mLastWord = newWord;
+
+			mWords.push_back(mLastWord);
+
+			mLastWord->addSpace(spaceWidth);
+		}
+		else
+		{
+			TextWord* newWord = CM_NEW(TextWord, ScratchAlloc) TextWord(true); // Each space is counted as its own word, to make certain operations easier
+			mLastWord = newWord;
+
+			mWords.push_back(mLastWord);
+
+			mLastWord->addSpace(spaceWidth);
+		}
+
+		calculateBounds();
+	}
+
+	void TextUtility::TextLine::addWord(TextWord* word)
+	{
+		mWords.push_back(word);
+		mLastWord = word;
+
+		calculateBounds();
+	}
+
+	TextUtility::TextWord* TextUtility::TextLine::removeLastWord()
+	{
+		if(mWords.size() == 0)
+			return nullptr;
+
+		TextWord* word = mWords[mWords.size() - 1];
+		mWords.erase(mWords.end() - 1);
+
+		if(mWords.size() > 0)
+			mLastWord = mWords[mWords.size() - 1];
+		else
+			mLastWord = nullptr;
+
+		calculateBounds();
+
+		return word;
+	}
+
+	std::vector<UINT32> TextUtility::TextLine::getNumQuadsPerPage() const
+	{
+		std::vector<UINT32> quadsPerPage;
+		for(auto wordIter = mWords.begin(); wordIter != mWords.end(); ++wordIter)
+		{
+			if(!(*wordIter)->isSpacer())
+			{
+				const vector<CHAR_DESC>::type& chars = (*wordIter)->getChars();
+				UINT32 kerning = 0;
+				for(auto charIter = chars.begin(); charIter != chars.end(); ++charIter)
+				{
+					if(charIter->page > (UINT32)quadsPerPage.size())
+						quadsPerPage.resize(charIter->page + 1);
+
+					quadsPerPage[charIter->page]++;
+				}
+			}
+		}
+
+		return quadsPerPage;
+	}
+
+	void TextUtility::TextLine::fillBuffer(UINT32 page, Vector2* vertices, Vector2* uvs, UINT32* indexes, UINT32 offset, UINT32 size, const FontData& fontData) const
+	{
+		UINT32 penX = 0;
+		UINT32 baselineY = fontData.fontDesc.baselineOffset;
+		for(auto wordIter = mWords.begin(); wordIter != mWords.end(); ++wordIter)
+		{
+			if((*wordIter)->isSpacer())
+			{
+				penX += fontData.fontDesc.spaceWidth;
+			}
+			else
+			{
+				const vector<CHAR_DESC>::type& chars = (*wordIter)->getChars();
+				UINT32 kerning = 0;
+				for(auto charIter = chars.begin(); charIter != chars.end(); ++charIter)
+				{
+					INT32 curX = penX + charIter->xOffset;
+					INT32 curY = ((INT32)baselineY - charIter->yOffset);
+
+					penX += charIter->xAdvance + kerning;
+					
+					kerning = 0;
+					if((charIter + 1) != chars.end())
+					{
+						for(size_t j = 0; j < charIter->kerningPairs.size(); j++)
+						{
+							if(charIter->kerningPairs[j].otherCharId == (charIter + 1)->charId)
+							{
+								kerning = charIter->kerningPairs[j].amount;
+								break;
+							}
+						}
+					}
+
+					if(charIter->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 + charIter->width), (float)curY);
+					vertices[curVert + 2] = Vector2((float)curX, (float)curY + (float)charIter->height);
+					vertices[curVert + 3] = Vector2((float)(curX + charIter->width), (float)curY + (float)charIter->height);
+
+					uvs[curVert + 0] = Vector2(charIter->uvX, charIter->uvY);
+					uvs[curVert + 1] = Vector2(charIter->uvX + charIter->uvWidth, charIter->uvY);
+					uvs[curVert + 2] = Vector2(charIter->uvX, charIter->uvY + charIter->uvHeight);
+					uvs[curVert + 3] = Vector2(charIter->uvX + charIter->uvWidth, charIter->uvY + charIter->uvHeight);
+
+					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++;
+				}
+			}
+		}
+	}
+
+	void TextUtility::TextLine::calculateBounds()
+	{
+		mWidth = 0;
+		mHeight = 0;
+		for(auto iter = mWords.begin(); iter != mWords.end(); ++iter)
+		{
+			mWidth += (*iter)->getWidth();
+			mHeight = std::max(mHeight, (*iter)->getHeight());
+		}
+	}
+
+	TextUtility::TextData::~TextData()
+	{
+		for(size_t i = 0; i < mLines.size(); i++)
+			CM_DELETE(mLines[i], TextLine, ScratchAlloc);
+	}
+
+	std::shared_ptr<TextUtility::TextData> TextUtility::getTextData(const String& text, const HFont& font, UINT32 fontSize, UINT32 width, UINT32 height, bool wordWrap)
+	{
+		const FontData* fontData = nullptr;
+		if(font != nullptr)
+		{
+			UINT32 nearestSize = font->getClosestAvailableSize(fontSize);
+			fontData = font->getFontDataForSize(nearestSize);
+		}
+
+		if(fontData == nullptr)
+			return nullptr;
+
+		if(fontData->size != fontSize)
+		{
+			LOGWRN("Unable to find font with specified size (" + toString(fontSize) + "). Using nearest available size: " + toString(fontData->size));
+		}
+
+		bool heightIsLimited = height > 0;
+		bool widthIsLimited = width > 0;
+
+		std::shared_ptr<TextUtility::TextData> textData(CM_NEW(TextData, PoolAlloc) TextData(), &MemAllocDeleter<TextData, PoolAlloc>::deleter);
+		TextLine* curLine = CM_NEW(TextLine, ScratchAlloc) TextLine();
+		textData->mLines.push_back(curLine);
+
+		UINT32 curHeight = fontData->fontDesc.lineHeight;
+		UINT32 charIdx = 0;
+
+		while(true)
+		{
+			if(charIdx >= text.size())
+				break;
+
+			if(text[charIdx] == '\n')
+			{
+				if(heightIsLimited && (curHeight + fontData->fontDesc.lineHeight * 2) > height)
+					break; // Max height reached
+
+				curLine = CM_NEW(TextLine, ScratchAlloc) TextLine();
+				textData->mLines.push_back(curLine);
+				curHeight += fontData->fontDesc.lineHeight;
+
+				charIdx++;
+				continue;
+			}
+
+			UINT32 charId = text[charIdx];
+			const CHAR_DESC& charDesc = fontData->getCharDesc(charId);
+
+			if(charId != SPACE_CHAR)
+			{
+				curLine->add(charDesc);
+
+				if(charDesc.page >= (UINT32)textData->mQuadsPerPage.size())
+					textData->mQuadsPerPage.resize(charDesc.page + 1);
+
+				textData->mQuadsPerPage[charDesc.page]++;
+			}
+			else
+				curLine->addSpace(fontData->fontDesc.spaceWidth);
+
+			if(widthIsLimited && curLine->getWidth() > width)
+			{
+				if(wordWrap)
+				{
+					TextWord* lastWord = curLine->removeLastWord();
+					if(lastWord->isSpacer())
+						curLine->addWord(lastWord); // Spaces can stay on previous line even if they don't technically fit
+
+					// No more lines fit vertically so we're done
+					if(heightIsLimited && curHeight > height)
+						break;
+
+					curLine = CM_NEW(TextLine, ScratchAlloc) TextLine();
+					textData->mLines.push_back(curLine);
+					curHeight += fontData->fontDesc.lineHeight;
+
+					if(!lastWord->isSpacer())
+						curLine->addWord(lastWord);
+				}
+				else
+				{
+					// Nothing else we can do, chars don't fit so we are done
+					break;
+				}
+			}
+
+			charIdx++;
+		}
+
+		return textData;
+	}
+}