浏览代码

New one-line input caret system working

Marko Pintera 12 年之前
父节点
当前提交
adf169b718

+ 16 - 4
BansheeEngine/Include/BsGUIInputBox.h

@@ -7,13 +7,19 @@
 
 namespace BansheeEngine
 {
+	enum CaretPos
+	{
+		CARET_BEFORE,
+		CARET_AFTER
+	};
+
 	class BS_EXPORT GUIInputBox : public GUIElement
 	{
 	public:
 		static const CM::String& getGUITypeName();
 
-		static GUIInputBox* create(GUIWidget& parent, const GUIElementStyle* style = nullptr);
-		static GUIInputBox* create(GUIWidget& parent, const GUILayoutOptions& layoutOptions, const GUIElementStyle* style = nullptr);
+		static GUIInputBox* create(GUIWidget& parent, bool multiline = false, const GUIElementStyle* style = nullptr);
+		static GUIInputBox* create(GUIWidget& parent, const GUILayoutOptions& layoutOptions, bool multiline = false, const GUIElementStyle* style = nullptr);
 	protected:
 		~GUIInputBox();
 
@@ -69,7 +75,7 @@ namespace BansheeEngine
 		bool mCaretShown;
 		bool mSelectionShown;
 
-		GUIInputBox(GUIWidget& parent, const GUIElementStyle* style, const GUILayoutOptions& layoutOptions);
+		GUIInputBox(GUIWidget& parent, const GUIElementStyle* style, const GUILayoutOptions& layoutOptions, bool multiline);
 
 		virtual bool mouseEvent(const GUIMouseEvent& ev);
 		virtual bool keyEvent(const GUIKeyEvent& ev);
@@ -78,9 +84,16 @@ namespace BansheeEngine
 		Sprite* renderElemToSprite(CM::UINT32 renderElemIdx, CM::UINT32& localRenderElemIdx) const;
 
 		void showCaret(CM::UINT32 charIdx);
+		void showCaretAtPos(const CM::Int2& pos);
 		void clearCaret();
 		CM::Int2 getCaretPosition() const;
 		CM::UINT32 getCaretHeight() const;
+		bool isCaretAtLineStart() const;
+
+		void moveCaretLeft();
+		void moveCaretRight();
+		void moveCaretToChar(CM::UINT32 charIdx, CaretPos caretPos);
+		CM::UINT32 getCharIdxAtCaretPos() const;
 
 		void showSelection(CM::UINT32 startChar, CM::UINT32 endChar);
 		void clearSelection();
@@ -88,6 +101,5 @@ namespace BansheeEngine
 
 		CM::Rect getTextBounds() const;
 		TEXT_SPRITE_DESC getTextDesc() const;
-		CM::UINT32 getValidCharCount() const;
 	};
 }

+ 1 - 1
BansheeEngine/Include/BsTextSprite.h

@@ -55,7 +55,7 @@ namespace BansheeEngine
 		const SpriteLineDesc& getLineDesc(CM::UINT32 lineIdx) const { return mLineDescs.at(lineIdx); }
 		CM::UINT32 getLineForChar(CM::UINT32 charIdx) const;
 		CM::Rect getCharRect(CM::UINT32 charIdx) const;
-		CM::UINT32 getCharIdxAtPos(const CM::Int2& pos) const;
+		CM::INT32 getCharIdxAtPos(const CM::Int2& pos) const;
 
 	private:
 		CM::Vector<SpriteLineDesc>::type mLineDescs;

+ 269 - 54
BansheeEngine/Source/BsGUIInputBox.cpp

@@ -24,10 +24,10 @@ namespace BansheeEngine
 		return name;
 	}
 
-	GUIInputBox::GUIInputBox(GUIWidget& parent, const GUIElementStyle* style, const GUILayoutOptions& layoutOptions)
+	GUIInputBox::GUIInputBox(GUIWidget& parent, const GUIElementStyle* style, const GUILayoutOptions& layoutOptions, bool multiline)
 		:GUIElement(parent, style, layoutOptions), mInputCursorSet(false), mDragInProgress(false),
 		mSelectionStart(0), mSelectionEnd(0), mSelectionAnchor(0), mCaretSprite(nullptr), mCaretShown(false), 
-		mSelectionShown(false), mCaretPos(0), mIsMultiline(false)
+		mSelectionShown(false), mCaretPos(0), mIsMultiline(multiline)
 	{
 		mImageSprite = cm_new<ImageSprite, PoolAlloc>();
 		mCaretSprite = cm_new<ImageSprite, PoolAlloc>();
@@ -57,7 +57,7 @@ namespace BansheeEngine
 			cm_delete(sprite);
 	}
 
-	GUIInputBox* GUIInputBox::create(GUIWidget& parent, const GUIElementStyle* style)
+	GUIInputBox* GUIInputBox::create(GUIWidget& parent, bool multiline, const GUIElementStyle* style)
 	{
 		if(style == nullptr)
 		{
@@ -65,10 +65,10 @@ namespace BansheeEngine
 			style = skin->getStyle(getGUITypeName());
 		}
 
-		return new (cm_alloc<GUIInputBox, PoolAlloc>()) GUIInputBox(parent, style, getDefaultLayoutOptions(style));
+		return new (cm_alloc<GUIInputBox, PoolAlloc>()) GUIInputBox(parent, style, getDefaultLayoutOptions(style), multiline);
 	}
 
-	GUIInputBox* GUIInputBox::create(GUIWidget& parent, const GUILayoutOptions& layoutOptions, const GUIElementStyle* style)
+	GUIInputBox* GUIInputBox::create(GUIWidget& parent, const GUILayoutOptions& layoutOptions, bool multiline, const GUIElementStyle* style)
 	{
 		if(style == nullptr)
 		{
@@ -76,7 +76,7 @@ namespace BansheeEngine
 			style = skin->getStyle(getGUITypeName());
 		}
 
-		return new (cm_alloc<GUIInputBox, PoolAlloc>()) GUIInputBox(parent, style, layoutOptions);
+		return new (cm_alloc<GUIInputBox, PoolAlloc>()) GUIInputBox(parent, style, layoutOptions, multiline);
 	}
 
 	UINT32 GUIInputBox::getNumRenderElements() const
@@ -84,8 +84,7 @@ namespace BansheeEngine
 		UINT32 numElements = mImageSprite->getNumRenderElements();
 		numElements += mTextSprite->getNumRenderElements();
 
-		/*if(mCaretShown && GUIManager::instance().getCaretBlinkState())*/
-		if(mCaretShown)
+		if(mCaretShown && GUIManager::instance().getCaretBlinkState())
 			numElements += mCaretSprite->getNumRenderElements();
 
 		if(mSelectionShown)
@@ -128,8 +127,7 @@ namespace BansheeEngine
 		TEXT_SPRITE_DESC textDesc = getTextDesc();
 		mTextSprite->update(textDesc);
 
-		/*if(mCaretShown && GUIManager::instance().getCaretBlinkState())*/
-		if(mCaretShown)
+		if(mCaretShown && GUIManager::instance().getCaretBlinkState())
 		{
 			IMAGE_SPRITE_DESC mCaretDesc;
 			mCaretDesc.offset = getCaretPosition();
@@ -149,7 +147,7 @@ namespace BansheeEngine
 
 			if(diff > 0)
 			{
-				for(INT32 i = 0; i < diff; i++)
+				for(UINT32 i = (UINT32)selectionRects.size(); i < (UINT32)mSelectionSprites.size(); i++)
 					cm_delete(mSelectionSprites[i]);
 
 				mSelectionSprites.erase(mSelectionSprites.begin() + selectionRects.size(), mSelectionSprites.end());
@@ -198,8 +196,7 @@ namespace BansheeEngine
 			return mImageSprite;
 		}
 
-		/*if(mCaretShown && GUIManager::instance().getCaretBlinkState())*/
-		if(mCaretShown)
+		if(mCaretShown && GUIManager::instance().getCaretBlinkState())
 		{
 			oldNumElements = newNumElements;
 			newNumElements += mCaretSprite->getNumRenderElements();
@@ -306,17 +303,8 @@ namespace BansheeEngine
 		{
 			mImageDesc.texture = mStyle->active.texture;
 
-			if(getValidCharCount() > 0)
-			{
-				UINT32 charIdx = mTextSprite->getCharIdxAtPos(ev.getPosition());
-				Rect charRect = mTextSprite->getCharRect(charIdx);
-
-				float xCenter = charRect.x + charRect.width * 0.5f;
-				if(ev.getPosition().x <= xCenter)
-					showCaret(charIdx);
-				else
-					showCaret(charIdx + 1);
-			}
+			if(mText.size() > 0)
+				showCaretAtPos(ev.getPosition());
 			else
 				showCaret(0);
 
@@ -367,20 +355,23 @@ namespace BansheeEngine
 		{
 			if(ev.getKey() == BC_BACK)
 			{
-				if(getValidCharCount() > 0)
+				if(mText.size() > 0)
 				{
 					if(mSelectionShown)
 					{
 						mText.erase(mText.begin() + mSelectionStart, mText.begin() + mSelectionEnd);
-						mCaretPos = mSelectionStart;
+						moveCaretToChar(mSelectionStart, CARET_BEFORE);
+
 						clearSelection();
 					}
 					else
 					{
-						if(mCaretPos > 0)
+						INT32 charIdx = getCharIdxAtCaretPos();
+
+						if(charIdx < (UINT32)mText.size())
 						{
-							mText.erase(mCaretPos - 1, 1);
-							mCaretPos--;
+							mText.erase(charIdx, 1);
+							moveCaretLeft();
 						}
 					}
 
@@ -392,17 +383,22 @@ namespace BansheeEngine
 
 			if(ev.getKey() == BC_DELETE)
 			{
-				if(getValidCharCount() > 0)
+				if(mText.size() > 0)
 				{
 					if(mSelectionShown)
 					{
 						mText.erase(mText.begin() + mSelectionStart, mText.begin() + mSelectionEnd);
-						mCaretPos = mSelectionStart;
+						moveCaretToChar(mSelectionStart, CARET_BEFORE);
+
 						clearSelection();
 					}
 					else
 					{
-						mText.erase(mCaretPos, 1);
+						UINT32 charIdx = getCharIdxAtCaretPos() + 1;
+						if(charIdx < (UINT32)mText.size())
+						{
+							mText.erase(charIdx, 1);
+						}
 					}
 
 					markAsDirty();
@@ -416,7 +412,7 @@ namespace BansheeEngine
 				if(ev.isShiftDown())
 				{
 					if(!mSelectionShown)
-						showSelection(mCaretPos, mCaretPos);
+						showSelection(getCharIdxAtCaretPos() + 1, getCharIdxAtCaretPos() + 1);
 
 					if(mSelectionAnchor == mSelectionEnd)
 						mSelectionStart = (UINT32)std::max(0, (INT32)mSelectionStart - 1);
@@ -429,7 +425,7 @@ namespace BansheeEngine
 				else
 				{
 					clearSelection();
-					mCaretPos = (UINT32)std::max(0, (INT32)mCaretPos - 1);
+					moveCaretLeft();
 
 					markAsDirty();
 					return true;
@@ -441,12 +437,12 @@ namespace BansheeEngine
 				if(ev.isShiftDown())
 				{
 					if(!mSelectionShown)
-						showSelection(mCaretPos, mCaretPos);
+						showSelection(getCharIdxAtCaretPos() + 1, getCharIdxAtCaretPos() + 1);
 
 					if(mSelectionAnchor == mSelectionStart)
-						mSelectionEnd = std::min(getValidCharCount(), mSelectionEnd + 1);
+						mSelectionEnd = std::min((UINT32)mText.size(), mSelectionEnd + 1);
 					else
-						mSelectionStart = std::min(getValidCharCount(), mSelectionStart + 1);
+						mSelectionStart = std::min((UINT32)mText.size(), mSelectionStart + 1);
 
 					markAsDirty();
 					return true;
@@ -454,13 +450,55 @@ namespace BansheeEngine
 				else
 				{
 					clearSelection();
-					mCaretPos = std::min(getValidCharCount(), mCaretPos + 1);
+					moveCaretRight();
 
 					markAsDirty();
 					return true;
 				}
 			}
 
+			if(ev.getKey() == BC_UP)
+			{
+				//Int2 caretCoords = getCaretPosition();
+				//UINT32 curLine = 0; mTextSprite->getLineForChar(mCaretPos);
+
+				//if(mCaretPos > 0)
+				//	curLine = 0; mTextSprite->getLineForChar(mCaretPos - 1);
+
+				//if(curLine > 0)
+				//{
+				//	if(ev.isShiftDown())
+				//	{
+				//		if(!mSelectionShown)
+				//			showSelection(mCaretPos, mCaretPos);
+
+				//		// TODO
+				//		//if(mSelectionAnchor == mSelectionEnd)
+				//		//	mSelectionStart = (UINT32)std::max(0, (INT32)mSelectionStart - 1);
+				//		//else
+				//		//	mSelectionEnd = (UINT32)std::max(0, (INT32)mSelectionEnd - 1);
+
+				//		markAsDirty();
+				//		return true;
+				//	}
+				//	else
+				//	{
+				//		clearSelection();
+
+				//		const SpriteLineDesc& lineDesc = mTextSprite->getLineDesc(curLine);
+				//		Int2 newCaretCoords = caretCoords;
+				//		newCaretCoords.y -= lineDesc.lineHeight / 2;
+
+				//		showCaretAtPos(newCaretCoords);
+				//	}
+				//}
+			}
+
+			if(ev.getKey() == BC_DOWN)
+			{
+				// TODO
+			}
+
 			if(ev.getKey() == BC_RETURN)
 			{
 				if(mIsMultiline)
@@ -468,11 +506,16 @@ namespace BansheeEngine
 					if(mSelectionShown)
 					{
 						mText.erase(mText.begin() + mSelectionStart, mText.begin() + mSelectionEnd);
-						mCaretPos = mSelectionStart;
+						moveCaretToChar(mSelectionStart, CARET_BEFORE);
 						clearSelection();
 					}
 
-					mText.insert(mText.begin() + mCaretPos, '\n');
+					if(mText.size() > 0)
+						mText.insert(mText.begin() + getCharIdxAtCaretPos() + 1, '\n');
+					else
+						mText.insert(mText.begin(), '\n');
+
+					moveCaretRight();
 
 					markAsDirty();
 					return true;
@@ -482,7 +525,7 @@ namespace BansheeEngine
 
 			if(ev.getKey() == BC_A && ev.isCtrlDown())
 			{
-				showSelection(0, getValidCharCount());
+				showSelection(0, (UINT32)mText.size());
 			}
 		}
 		else if(ev.getType() == GUIKeyEventType::TextInput)
@@ -490,12 +533,16 @@ namespace BansheeEngine
 			if(mSelectionShown)
 			{
 				mText.erase(mText.begin() + mSelectionStart, mText.begin() + mSelectionEnd);
-				mCaretPos = mSelectionStart;
+				moveCaretToChar(mSelectionStart, CARET_BEFORE);
 				clearSelection();
 			}
 
-			mText.insert(mText.begin() + mCaretPos, ev.getInputChar());
-			mCaretPos++;
+			if(mText.size() > 0)
+				mText.insert(mText.begin() + getCharIdxAtCaretPos() + 1, ev.getInputChar());
+			else
+				mText.insert(mText.begin(), ev.getInputChar());
+
+			moveCaretRight();
 
 			markAsDirty();
 			return true;
@@ -522,6 +569,43 @@ namespace BansheeEngine
 		markAsDirty();
 	}
 
+	void GUIInputBox::showCaretAtPos(const CM::Int2& pos)
+	{
+		INT32 charIdx = mTextSprite->getCharIdxAtPos(pos);
+
+		if(charIdx != -1)
+		{
+			Rect charRect = mTextSprite->getCharRect(charIdx);
+
+			float xCenter = charRect.x + charRect.width * 0.5f;
+			if(pos.x <= xCenter)
+				moveCaretToChar(charIdx, CARET_BEFORE);
+			else
+				moveCaretToChar(charIdx, CARET_AFTER);
+		}
+		else
+		{
+			UINT32 numLines = mTextSprite->getNumLines();
+
+			UINT32 curPos = 0;
+			for(UINT32 i = 0; i < numLines; i++)
+			{
+				const SpriteLineDesc& line = mTextSprite->getLineDesc(i);
+
+				if(pos.y >= line.lineYStart && pos.y < (line.lineYStart + (INT32)line.lineHeight))
+				{
+					mCaretPos = curPos;
+					return;
+				}
+
+				UINT32 numChars = line.endChar - line.startChar;
+				curPos += numChars;
+			}
+
+			mCaretPos = curPos;
+		}
+	}
+
 	void GUIInputBox::clearCaret()
 	{
 		mCaretShown = false;
@@ -530,10 +614,29 @@ namespace BansheeEngine
 
 	Int2 GUIInputBox::getCaretPosition() const
 	{
-		if(mCaretPos > 0)
+		if(mText.size() > 0)
 		{
-			Rect charRect = mTextSprite->getCharRect(mCaretPos - 1);
-			UINT32 lineIdx = mTextSprite->getLineForChar(mCaretPos - 1);
+			UINT32 curPos = 0;
+			UINT32 numLines = mTextSprite->getNumLines();
+			for(UINT32 i = 0; i < numLines; i++)
+			{
+				if(mCaretPos == curPos)
+				{
+					// Caret is on line start
+					Rect contentBounds = getTextBounds();
+					return Int2(contentBounds.x, mTextSprite->getLineDesc(i).lineYStart);
+				}
+
+				const SpriteLineDesc& lineDesc = mTextSprite->getLineDesc(i);
+				UINT32 numChars = lineDesc.endChar - lineDesc.startChar;
+				curPos += numChars;
+			}
+
+			UINT32 charIdx = getCharIdxAtCaretPos();
+			charIdx = std::min((UINT32)(mText.size() - 1), charIdx);
+
+			Rect charRect = mTextSprite->getCharRect(charIdx);
+			UINT32 lineIdx = mTextSprite->getLineForChar(charIdx);
 			UINT32 yOffset = mTextSprite->getLineDesc(lineIdx).lineYStart;
 
 			return Int2(charRect.x + charRect.width + 1, yOffset);
@@ -547,9 +650,11 @@ namespace BansheeEngine
 
 	UINT32 GUIInputBox::getCaretHeight() const
 	{
-		if(mCaretPos > 0)
+		UINT32 charIdx = getCharIdxAtCaretPos();
+
+		if(charIdx < (UINT32)mText.size())
 		{
-			UINT32 lineIdx = mTextSprite->getLineForChar(mCaretPos - 1);
+			UINT32 lineIdx = mTextSprite->getLineForChar(charIdx);
 			return mTextSprite->getLineDesc(lineIdx).lineHeight;
 		}
 		else
@@ -567,6 +672,121 @@ namespace BansheeEngine
 		return 0;
 	}
 
+	void GUIInputBox::moveCaretLeft()
+	{
+		mCaretPos = (UINT32)std::max(0, (INT32)mCaretPos - 1);
+
+		if(isCaretAtLineStart()) // Skip line start char as well
+			mCaretPos = (UINT32)std::max(0, (INT32)mCaretPos - 1);
+	}
+
+	void GUIInputBox::moveCaretRight()
+	{
+		//if(isCaretAtLineStart())
+		//	mCaretPos = mCaretPos + 2; // Skip line start char as well
+		//else
+			mCaretPos = mCaretPos + 1; // TODO - Limit this
+	}
+
+	void GUIInputBox::moveCaretToChar(UINT32 charIdx, CaretPos caretPos)
+	{
+		if(charIdx >= (UINT32)mText.size())
+		{
+			mCaretPos = 0;
+			return;
+		}
+
+		UINT32 numLines = mTextSprite->getNumLines();
+		UINT32 curPos = 0;
+		UINT32 curCharIdx = 0;
+		for(UINT32 i = 0; i < numLines; i++)
+		{
+			if(i == 0)
+				curPos++; // Beginning of the line has a special caret position, primarily so we can
+			// still place a caret on an empty line
+
+			const SpriteLineDesc& lineDesc = mTextSprite->getLineDesc(i);
+			UINT32 numChars = lineDesc.endChar - lineDesc.startChar;
+			if(charIdx > (curCharIdx + numChars))
+			{
+				curCharIdx += numChars;
+				curPos += numChars;
+				continue;
+			}
+
+			UINT32 diff = charIdx - curCharIdx;
+
+			if(caretPos == CARET_BEFORE)
+				curPos += diff - 1;
+			else
+				curPos += diff;
+
+			break;
+		}
+
+		showCaret(curPos);
+	}
+
+	UINT32 GUIInputBox::getCharIdxAtCaretPos() const
+	{
+		UINT32 numLines = mTextSprite->getNumLines();
+		UINT32 curPos = 0;
+		UINT32 curCharIdx = 0;
+		for(UINT32 i = 0; i < numLines; i++)
+		{
+			const SpriteLineDesc& lineDesc = mTextSprite->getLineDesc(i);
+
+			if(curPos == mCaretPos)
+			{
+				return lineDesc.startChar;
+			}
+
+			if(i == 0)
+				curPos++; // Beginning of the line has a special caret position, primarily so we can
+						  // still place a caret on an empty line
+
+			UINT32 numChars = lineDesc.endChar - lineDesc.startChar;
+			if(mCaretPos > (curPos + numChars))
+			{
+				curCharIdx += numChars;
+				curPos += numChars;
+				continue;
+			}
+
+			UINT32 diff = mCaretPos - curPos; 
+			curCharIdx += diff;
+
+			return curCharIdx;
+		}
+
+		return 0;
+	}
+
+	bool GUIInputBox::isCaretAtLineStart() const
+	{
+		UINT32 numLines = mTextSprite->getNumLines();
+		UINT32 curPos = 0;
+		for(UINT32 i = 0; i < numLines; i++)
+		{
+			const SpriteLineDesc& lineDesc = mTextSprite->getLineDesc(i);
+
+			if(curPos == mCaretPos)
+				return true;
+
+			if(i == 0)
+				curPos++; // Beginning of the line has a special caret position, primarily so we can
+						  // still place a caret on an empty line
+
+			UINT32 numChars = lineDesc.endChar - lineDesc.startChar;
+			curPos += numChars;
+
+			if(curPos > mCaretPos)
+				return false;
+		}
+
+		return false;
+	}
+
 	void GUIInputBox::showSelection(UINT32 startChar, UINT32 endChar)
 	{
 		mSelectionStart = startChar;
@@ -686,11 +906,6 @@ namespace BansheeEngine
 		return textDesc;
 	}
 
-	UINT32 GUIInputBox::getValidCharCount() const
-	{
-		return (UINT32)mText.size(); // TODO - Need to ignore newline chars
-	}
-
 	void GUIInputBox::_setFocus(bool focus)
 	{
 		if(focus)

+ 62 - 15
BansheeEngine/Source/BsTextSprite.cpp

@@ -15,6 +15,8 @@ namespace BansheeEngine
 
 	void TextSprite::update(const TEXT_SPRITE_DESC& desc)
 	{
+		mLineDescs.clear();
+
 		std::shared_ptr<TextUtility::TextData> textData = TextUtility::getTextData(desc.text, desc.font, desc.fontSize, desc.width, desc.height, desc.wordWrap);
 
 		if(textData == nullptr)
@@ -162,14 +164,16 @@ namespace BansheeEngine
 		}
 
 		// Store cached line data
-		mLineDescs.clear();
 		UINT32 curCharIdx = 0;
 		UINT32 cachedLineY = 0;
+		UINT32 curLineIdx = 0;
 		for(auto& line : lines)
 		{
+			UINT32 newlineChar = (curLineIdx == ((UINT32)lines.size() - 1)) ? 0 : 1;
+
 			SpriteLineDesc lineDesc;
 			lineDesc.startChar = curCharIdx;
-			lineDesc.endChar = curCharIdx + line.getNumChars();
+			lineDesc.endChar = curCharIdx + line.getNumChars() + newlineChar;
 			lineDesc.lineHeight = line.getYOffset();
 			lineDesc.lineYStart = vertOffset + cachedLineY + desc.offset.y;
 
@@ -177,6 +181,7 @@ namespace BansheeEngine
 
 			curCharIdx = lineDesc.endChar;
 			cachedLineY += lineDesc.lineHeight;
+			curLineIdx++;
 		}
 
 		updateBounds();
@@ -184,12 +189,22 @@ namespace BansheeEngine
 
 	CM::Rect TextSprite::getCharRect(UINT32 charIdx) const
 	{
-		UINT32 idx = 0;
+		UINT32 lineIdx = getLineForChar(charIdx);
+
+		// If char is newline we don't have any geometry to return
+		const SpriteLineDesc& lineDesc = getLineDesc(lineIdx);
+		if(lineIdx != (getNumLines() - 1) && lineDesc.endChar == (charIdx - 1))
+			return Rect();
+
+		UINT32 numNewlineChars = lineIdx;
+		UINT32 quadIdx = charIdx - numNewlineChars;
+
+		UINT32 curQuadIdx = 0;
 		for(auto& renderElem : mCachedRenderElements)
 		{
-			if(charIdx >= idx && charIdx < renderElem.numQuads)
+			if(quadIdx >= curQuadIdx && quadIdx < renderElem.numQuads)
 			{
-				UINT32 localIdx = (charIdx - idx) * 4;
+				UINT32 localIdx = (quadIdx - curQuadIdx) * 4;
 
 				Rect charRect;
 				charRect.x = Math::RoundToInt(renderElem.vertices[localIdx + 0].x);
@@ -200,40 +215,72 @@ namespace BansheeEngine
 				return charRect;
 			}
 
-			idx += renderElem.numQuads;
+			curQuadIdx += renderElem.numQuads;
 		}
 
 		CM_EXCEPT(InternalErrorException, "Invalid character index: " + toString(charIdx));
 	}
 
-	UINT32 TextSprite::getCharIdxAtPos(const Int2& pos) const
+	INT32 TextSprite::getCharIdxAtPos(const Int2& pos) const
 	{
 		Vector2 vecPos((float)pos.x, (float)pos.y);
 
+		UINT32 lineStartChar = 0;
+		UINT32 lineEndChar = 0;
+		UINT32 newlineChars = 0;
+		for(auto& line : mLineDescs)
+		{
+			if(pos.y >= line.lineYStart && pos.y < (line.lineYStart + (INT32)line.lineHeight))
+			{
+				lineStartChar = line.startChar;
+				lineEndChar = line.endChar;
+				break;
+			}
+
+			newlineChars++; // Newline chars count in the startChar/endChar variables, but don't actually exist in the buffers
+			// so we need to filter them out
+		}
+
+		UINT32 lineStartQuad = lineStartChar - newlineChars;
+		UINT32 lineEndQuad = lineEndChar - newlineChars;
+
 		float nearestDist = std::numeric_limits<float>::max();
 		UINT32 nearestChar = 0;
-		UINT32 idx = 0;
+		UINT32 quadIdx = 0;
+		bool foundChar = false;
 		for(auto& renderElem : mCachedRenderElements)
 		{
 			for(UINT32 i = 0; i < renderElem.numQuads; i++)
 			{
-				UINT32 curVert = idx * 4;
+				if(quadIdx < lineStartQuad)
+				{
+					quadIdx++;
+					continue;
+				}
+
+				if(quadIdx >= lineEndQuad)
+					break;
 
-				Vector2 center = renderElem.vertices[curVert + 0] + renderElem.vertices[curVert + 1] + 
-					renderElem.vertices[curVert + 2] + renderElem.vertices[curVert + 3];
-				center /= 4;
+				UINT32 curVert = quadIdx * 4;
 
-				float dist = center.squaredDistance(vecPos);
+				float centerX = renderElem.vertices[curVert + 0].x + renderElem.vertices[curVert + 1].x;
+				centerX *= 0.5f;
+
+				float dist = Math::Abs(centerX - vecPos.x);
 				if(dist < nearestDist)
 				{
-					nearestChar = idx;
+					nearestChar = quadIdx;
 					nearestDist = dist;
+					foundChar = true;
 				}
 
-				idx++;
+				quadIdx++;
 			}
 		}
 
+		if(!foundChar)
+			return -1;
+
 		return nearestChar;
 	}
 

+ 1 - 0
CamelotClient/CmEditorWindow.cpp

@@ -72,6 +72,7 @@ namespace BansheeEditor
 		//layout.addElement(mDbgLabel);
 
 		layout.addElement(GUIInputBox::create(*mGUI));
+		layout.addElement(GUIInputBox::create(*mGUI, GUILayoutOptions::fixed(100, 100), true));
 		
 		//GUIFlexibleSpace& space4 = otherLayout.addFlexibleSpace();
 		//otherLayout.addElement(mDbgLabel);

+ 3 - 0
CamelotCore/Include/CmTextUtility.h

@@ -119,5 +119,8 @@ namespace CamelotFramework
 		};
 
 		static std::shared_ptr<TextUtility::TextData> getTextData(const WString& text, const HFont& font, UINT32 fontSize, UINT32 width = 0, UINT32 height = 0, bool wordWrap = false);
+
+	private:
+		static void addCharToPage(TextData& data, UINT32 page, const FontData& fontData);
 	};
 }

+ 19 - 25
CamelotCore/Source/CmTextUtility.cpp

@@ -354,6 +354,9 @@ namespace CamelotFramework
 			if(charIdx >= text.size())
 				break;
 
+			UINT32 charId = text[charIdx];
+			const CHAR_DESC& charDesc = fontData->getCharDesc(charId);
+
 			if(text[charIdx] == '\n')
 			{
 				if(heightIsLimited && (curHeight + fontData->fontDesc.lineHeight * 2) > height)
@@ -368,38 +371,15 @@ namespace CamelotFramework
 				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->mTexturePages.resize(charDesc.page + 1);
-				}
-
-				textData->mQuadsPerPage[charDesc.page]++;
-
-				if(textData->mTexturePages[charDesc.page] == nullptr)
-					textData->mTexturePages[charDesc.page] = fontData->texturePages[charDesc.page];
+				addCharToPage(*textData, charDesc.page, *fontData);
 			}
 			else
 			{
 				curLine->addSpace();
-
-				if((UINT32)textData->mQuadsPerPage.size() == 0)
-				{
-					textData->mQuadsPerPage.resize(1);
-					textData->mTexturePages.resize(1);
-				}
-
-				textData->mQuadsPerPage[0]++;
-
-				if(textData->mTexturePages[0] == nullptr)
-					textData->mTexturePages[0] = fontData->texturePages[0];
+				addCharToPage(*textData, 0, *fontData);
 			}
 
 			if(widthIsLimited && curLine->getWidth() > width)
@@ -435,6 +415,20 @@ namespace CamelotFramework
 		return textData;
 	}
 
+	void TextUtility::addCharToPage(TextUtility::TextData& data, UINT32 page, const FontData& fontData)
+	{
+		if(page >= (UINT32)data.mQuadsPerPage.size())
+		{
+			data.mQuadsPerPage.resize(page + 1);
+			data.mTexturePages.resize(page + 1);
+		}
+
+		data.mQuadsPerPage[page]++;
+
+		if(data.mTexturePages[page] == nullptr)
+			data.mTexturePages[page] = fontData.texturePages[page];
+	}
+
 	UINT32 TextUtility::TextData::getWidth() const
 	{
 		UINT32 width = 0;

+ 3 - 4
TODO.txt

@@ -26,10 +26,9 @@ IMMEDIATE:
 	- SpriteTexture keeps a static reference to DUmmyTexture which I need to release before shutdown
 
 TextBox needed elements:
- - Implement and test multiline text control
- - Test selection (especially multiline selection)
- - I need to replace mText with clean text without \n because newline will break my char indexes
- - Ctrl+A doesn't work. Only TextInput event is sent, without button press
+ - Up/Down arrow keys should move cursor up/down
+ - Drag mouse to update selection
+ - Need to figure out how do I place a cursor on an empty first line?
  - Text scroll. Typing outside of textbox should scroll the text so caret is visible.
  - Get DebugCamera to ignore input if GUI has already processed it
  - Cut/Copy/Paste