I'm creating a textbox for my UI, and I found a way to place the cursor where the user clicks on the text.
Here is all of my code:
using System;
using System.Collections.Generic;
using System.Timers;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace TextEditorTest
{
public class Cursor
{
public int CharacterIndex = 0;
public string Symbol = "|";
public Vector2 Position = new Vector2(0, 0);
public int LineIndex = 0;
public bool Visible = true;
}
/// <summary>
/// This is the main type for your game.
/// </summary>
public class Game1 : Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
private GapBuffer<string> m_Text = new GapBuffer<string>();
//private List<Vector2> m_CharacterPositions = new List<Vector2>();
private Dictionary<int, Vector2> m_CharacterPositions = new Dictionary<int, Vector2>();
private List<Rectangle> m_HitBoxes = new List<Rectangle>();
private bool m_UpdateCharPositions = true;
private const int NUM_CHARS_IN_LINE = 10;
private Vector2 m_TextEditPosition = new Vector2(50, 0), m_TextEditSize = new Vector2(250, 320);
private SpriteFont m_Font;
private Timer m_CursorVisibilityTimer = new Timer();
private Cursor m_Cursor = new Cursor();
private InputHelper m_Input = new InputHelper();
private int m_NumLinesInText = 1;
private bool m_HasFocus = true;
private bool m_MultiLine = true;
private bool m_CapitalLetters = false;
//For how long has a key been presseed?
private DateTime m_DownSince = DateTime.Now;
private float m_TimeUntilRepInMillis = 100f;
private int m_RepsPerSec = 15;
private DateTime m_LastRep = DateTime.Now;
private Keys? m_RepChar; //A character currently being pressed (repeated).
private Vector2 m_TextPosition = new Vector2(0, 0); //Coordinate for anchoring the text.
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
m_Cursor.Position = m_TextEditPosition;
m_CursorVisibilityTimer = new Timer(100);
m_CursorVisibilityTimer.Enabled = true;
m_CursorVisibilityTimer.Elapsed += CursorVisibilityTimer_Elapsed;
m_CursorVisibilityTimer.Start();
IsMouseVisible = true;
Window.TextInput += Window_TextInput;
m_TextPosition = m_TextEditPosition;
}
private void CursorVisibilityTimer_Elapsed(object sender, ElapsedEventArgs e)
{
if (m_HasFocus)
{
if (m_Cursor.Visible)
m_Cursor.Visible = false;
else
m_Cursor.Visible = true;
}
}
/// <summary>
/// The text in this TextEditor instance, containing "\n".
/// </summary>
private string TextWithLBreaks
{
get
{
string Text = "";
foreach (string Str in m_Text)
Text += Str;
return Text;
}
}
/// <summary>
/// Returns a line of text.
/// </summary>
/// <param name="LineIndex">The index of the line of text to get.</param>
/// <returns>The line of text, as indicated by the line index.</returns>
private string GetLine(int LineIndex)
{
try
{
if (TextWithLBreaks.Contains("\n"))
return TextWithLBreaks.Split("\n".ToCharArray())[LineIndex];
}
catch(Exception)
{
return "";
}
return TextWithLBreaks;
}
/// <summary>
/// Make sure cursor's index is valid and in range.
/// </summary>
private void FixCursorIndex()
{
if (m_Cursor.CharacterIndex < 0)
m_Cursor.CharacterIndex = 0;
if (m_Cursor.CharacterIndex == m_Text.Count)
m_Cursor.CharacterIndex = m_Text.Count - 1;
}
/// <summary>
/// Make sure cursor's position is valid and in range.
/// </summary>
private void FixCursorPosition()
{
if (m_Cursor.Position.X < m_TextEditPosition.X)
m_Cursor.Position.X = m_TextEditPosition.X;
m_UpdateCharPositions = true;
UpdateCharacterPositions();
UpdateHitboxes();
//Find the curor's real character index.
int RealCharIndex = m_Cursor.CharacterIndex;
RealCharIndex = (RealCharIndex < m_CharacterPositions.Count) ? //Make sure it doesn't overflow.
RealCharIndex : m_CharacterPositions.Count - 1;
if (RealCharIndex < 0)
RealCharIndex = 0; //Make sure it doesn't underflow.
//Adjust the character's position based on the real character index.
if (m_Text.Count > 0)
{
Vector2 IndexPosition = m_CharacterPositions[(RealCharIndex > 0) ? RealCharIndex : 0];
if (m_Cursor.Position.X < IndexPosition.X)
m_Cursor.Position.X = IndexPosition.X;
if (m_Cursor.Position.Y != IndexPosition.Y)
m_Cursor.Position.Y = IndexPosition.Y;
}
}
/// <summary>
/// The text in this TextEditor instance, without \n
/// (except for those explicitly added by pressing backspace).
/// </summary>
public string Text
{
get
{
string Text = "";
foreach (string Str in m_Text)
Text += Str;
return Text.Replace("\n", "");
}
}
/// <summary>
/// Returns the width of a character in this font.
/// </summary>
/// <returns>The width of the character in floating point numbers.</returns>
private float CharacterWidth
{
get { return m_Font.MeasureString("a").X; }
}
/// <summary>
/// Returns the width of a capitalized character in this font.
/// </summary>
/// <returns>The width of the capitalized character in floating point numbers.</returns>
private float CapitalCharacterWidth
{
get { return m_Font.MeasureString("A").X; }
}
/// <summary>
/// Returns the height of a character in this font.
/// </summary>
/// <returns>The height of the character in floating point numbers.</returns>
private float CharacterHeight
{
get { return m_Font.MeasureString("a").Y; }
}
/// <summary>
/// Returns the height of a capitalized character in this font.
/// </summary>
/// <returns>The height of the capitalized character in floating point numbers.</returns>
private float CapitalCharacterHeight
{
get { return m_Font.MeasureString("A").Y; }
}
/// <summary>
/// Returns the last line of text in the gap buffer.
/// </summary>
/// <returns></returns>
private string CurrentLine
{
get
{
if (m_Text.Count > 1)
{
if (TextWithLBreaks.Contains("\n"))
{
string[] Lines = TextWithLBreaks.Split("\n".ToCharArray());
return Lines[Lines.Length - 1];
}
else
return TextWithLBreaks;
}
if (m_Text.Count > 0)
return m_Text[0];
else
return "";
}
}
/// <summary>
/// The control received text input.
/// </summary>
private void Window_TextInput(object sender, TextInputEventArgs e)
{
if (e.Character != (char)Keys.Back)
{
int Index = TextWithLBreaks.LastIndexOf("\n", m_Cursor.CharacterIndex);
if (Index == -1) //No occurence was found!!
{
if (Text.Length <= NUM_CHARS_IN_LINE)
{
AddText((m_CapitalLetters == true) ? e.Character.ToString().ToUpper() :
e.Character.ToString());
m_CapitalLetters = false;
m_UpdateCharPositions = true;
return;
}
else
{
if (m_MultiLine)
{
AddNewline();
return;
}
}
}
if ((m_Cursor.CharacterIndex - Index) <= NUM_CHARS_IN_LINE)
{
//If the cursor has moved away from the end of the text...
if (m_Cursor.CharacterIndex < (m_Text.Count - (1 + m_NumLinesInText)))
{
//... insert it at the cursor's position.
m_Text.Insert(m_Cursor.CharacterIndex, (m_CapitalLetters == true) ? e.Character.ToString().ToUpper() :
e.Character.ToString());
m_CapitalLetters = false;
m_UpdateCharPositions = true;
}
else
{
AddText((m_CapitalLetters == true) ? e.Character.ToString().ToUpper() :
e.Character.ToString()); //... just add the text as usual.
m_CapitalLetters = false;
m_UpdateCharPositions = true;
}
}
else
{
if(m_MultiLine)
AddNewline();
}
}
}
/// <summary>
/// Adds a string to m_Text, and updates the cursor.
/// </summary>
/// <param name="Text">The string to add.</param>
private void AddText(string Text)
{
m_Text.Add(Text);
m_Cursor.CharacterIndex++;
m_Cursor.Position.X += CharacterWidth;
}
//Can the cursor move further down or has it reached the end of the textbox?
private bool m_CanMoveCursorDown = true;
/// <summary>
/// Adds a newline to m_Text, and updates the cursor.
/// </summary>
private void AddNewline()
{
m_Text.Add("\n");
m_Cursor.CharacterIndex++;
m_Cursor.Position.X = m_TextEditPosition.X;
m_Cursor.LineIndex++;
//Scroll the text up if it's gone beyond the borders of the control.
if ((m_TextEditPosition.Y - TextSize().Y) < (m_TextEditPosition.Y - m_TextEditSize.Y))
{
m_TextPosition.Y -= CapitalCharacterHeight;
m_CanMoveCursorDown = false;
m_UpdateCharPositions = true;
}
if (m_CanMoveCursorDown)
m_Cursor.Position.Y += CapitalCharacterHeight;
m_NumLinesInText++;
}
/// <summary>
/// Removes text from m_Text.
/// </summary>
private void RemoveText()
{
FixCursorIndex();
FixCursorPosition();
if (m_Cursor.Position.X > m_TextEditPosition.X)
{
m_Text.RemoveAt(m_Cursor.CharacterIndex);
m_Cursor.CharacterIndex--;
m_Cursor.Position.X -= CharacterWidth;
}
if (m_Cursor.Position.X <= m_TextEditPosition.X)
{
if (m_Cursor.LineIndex != 0)
{
m_Cursor.Position.X = m_TextEditPosition.X +
m_Font.MeasureString(GetLine(m_Cursor.LineIndex - 1)).X;
if (m_MultiLine)
{
m_Cursor.Position.Y -= CapitalCharacterHeight;
m_Cursor.LineIndex--;
m_NumLinesInText--;
if (m_TextPosition.Y < m_TextEditPosition.Y)
m_TextPosition.Y += m_Font.LineSpacing;
}
}
}
}
/// <summary>
/// Moves m_Cursor left.
/// </summary>
private void MoveCursorLeft()
{
if (m_Cursor.Position.X > m_TextEditPosition.X)
{
m_Cursor.CharacterIndex -= (((NUM_CHARS_IN_LINE + 1) -
GetLine(m_Cursor.LineIndex).Length) + GetLine(m_Cursor.LineIndex).Length);
m_Cursor.Position.X -= CapitalCharacterHeight;
}
//Scroll the text right if the cursor is at the beginning of the control.
if (m_Cursor.Position.X == m_TextEditPosition.X)
{
if (m_TextPosition.X > m_TextEditPosition.X)
m_TextPosition.X -= m_Font.LineSpacing;
}
}
/// <summary>
/// Moves m_Cursor right.
/// </summary>
private void MoveCursorRight()
{
if (m_Cursor.Position.X < (m_TextEditPosition.X + m_TextEditSize.X))
{
m_Cursor.CharacterIndex += (((NUM_CHARS_IN_LINE + 1) -
GetLine(m_Cursor.LineIndex).Length) + GetLine(m_Cursor.LineIndex).Length);
m_Cursor.Position.X += CapitalCharacterHeight;
}
//Scroll the text right if the cursor is at the beginning of the control.
if (m_Cursor.Position.X == m_TextEditPosition.X)
{
if (m_TextPosition.X < m_TextEditPosition.X)
m_TextPosition.X += m_Font.LineSpacing;
}
}
/// <summary>
/// Moves m_Cursor up.
/// </summary>
private void MoveCursorUp()
{
if (m_Cursor.Position.Y > m_TextEditPosition.Y)
{
m_Cursor.LineIndex--;
m_Cursor.CharacterIndex -= (((NUM_CHARS_IN_LINE + 1) -
GetLine(m_Cursor.LineIndex).Length) + GetLine(m_Cursor.LineIndex).Length);
m_Cursor.Position.Y -= CapitalCharacterHeight;
m_CanMoveCursorDown = true;
}
//Scroll the text down if the cursor is at the top of the control.
if (m_Cursor.Position.Y == m_TextEditPosition.Y)
{
if (m_TextPosition.Y < m_TextEditPosition.Y)
m_TextPosition.Y += m_Font.LineSpacing;
}
}
/// <summary>
/// Moves m_Cursor down.
/// </summary>
private void MoveCursorDown()
{
if (m_Cursor.Position.Y < (m_TextEditPosition.Y + m_TextEditSize.Y))
{
m_Cursor.LineIndex++;
m_Cursor.CharacterIndex += (((NUM_CHARS_IN_LINE + 1) -
GetLine(m_Cursor.LineIndex).Length) + GetLine(m_Cursor.LineIndex).Length);
m_Cursor.Position.Y += CapitalCharacterHeight;
}
else //Scroll the text up if the cursor is at the bottom of the control.
{
if ((m_TextPosition.Y + TextSize().Y) > (m_TextEditPosition.Y + m_TextEditSize.Y))
m_TextPosition.Y -= m_Font.LineSpacing;
}
}
/// <summary>
/// Allows the game to perform any initialization it needs to before starting to run.
/// This is where it can query for any required services and load any non-graphic
/// related content. Calling base.Initialize will enumerate through any components
/// and initialize them as well.
/// </summary>
protected override void Initialize()
{
// TODO: Add your initialization logic here
base.Initialize();
}
/// <summary>
/// LoadContent will be called once per game and is the place to load
/// all of your content.
/// </summary>
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
m_Font = Content.Load<SpriteFont>("ProjectDollhouse_11px");
}
/// <summary>
/// UnloadContent will be called once per game and is the place to unload
/// game-specific content.
/// </summary>
protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
}
/// <summary>
/// Calculates the size of all the text in the textbox.
/// </summary>
/// <returns>A Vector2 containing the width and height of the text.</returns>
private Vector2 TextSize()
{
float Width = 0.0f, Height = 0.0f;
foreach (string Str in TextWithLBreaks.Split("\n".ToCharArray()))
{
Vector2 Size = m_Font.MeasureString(Str);
Width = Size.X;
Height += Size.Y;
}
return new Vector2(Width, Height);
}
/// <summary>
/// Update the hitboxes for the characters in the textbox.
/// The hitboxes are used to detect collision(s) with the mouse cursor.
/// </summary>
private void UpdateHitboxes()
{
if (m_UpdateCharPositions)
{
int Height = 0;
m_HitBoxes.Clear();
//Make sure it doesn't go out of bounds...
if (m_Text.Count >= 1)
{
for (int i = 0; i < m_CharacterPositions.Count; i++)
{
//Make sure it doesn't go out of bounds...
Height = (int)m_Font.MeasureString(m_Text[i < m_Text.Count ? i : m_Text.Count - 1]).Y;
//Create a hitbox for each character if the character isn't the last one.
if (i != m_CharacterPositions.Count - 1)
{
Rectangle Hitbox = new Rectangle((int)m_CharacterPositions[i].X, (int)m_CharacterPositions[i].Y,
(int)(m_CharacterPositions[i + 1].X - m_CharacterPositions[i].X), Height);
m_HitBoxes.Add(Hitbox);
}
}
}
m_UpdateCharPositions = false;
}
}
/// <summary>
/// Updates the positions of the characters.
/// Called when a character is added or deleted from the textbox.
/// </summary>
private void UpdateCharacterPositions()
{
Vector2 Position = m_TextEditPosition;
float XPosition = 0, YPosition = 0;
if (m_UpdateCharPositions)
{
m_CharacterPositions.Clear();
int CharIndex = 0;
foreach (string Str in TextWithLBreaks.Split("\n".ToCharArray()))
{
XPosition = 0;
for (int i = 0; i < Str.Length; i++)
{
float CharWidth = m_Font.MeasureString(Str.Substring(i, 1)).X;
XPosition += CharWidth;
m_CharacterPositions.Add(CharIndex, new Vector2(XPosition + m_TextEditPosition.X, Position.Y + m_TextEditPosition.Y));
CharIndex++;
}
YPosition += CapitalCharacterHeight;
Position.Y = YPosition;
}
///This shouldn't be set here, because it is set in UpdateHitboxes();
//m_UpdateCharPositions = false;
}
}
/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
m_Input.Update();
UpdateCharacterPositions();
UpdateHitboxes();
foreach (Rectangle Hitbox in m_HitBoxes)
{
if (Hitbox.Contains(new Vector2(m_Input.MousePosition.X, m_Input.MousePosition.Y)) &&
m_Input.IsNewPress(MouseButtons.LeftButton))
{
m_Cursor.Position = new Vector2(Hitbox.X, Hitbox.Y);
int CharIndex = m_CharacterPositions.FirstOrDefault(x => x.Value == m_Cursor.Position).Key;
if (CharIndex != -1)
m_Cursor.CharacterIndex = CharIndex;
}
}
foreach (Keys Key in (Keys[])Enum.GetValues(typeof(Keys)))
{
if (m_Input.IsNewPress(Key))
{
m_DownSince = DateTime.Now;
m_RepChar = Key;
}
else if (m_Input.IsOldPress(Key))
{
if (m_RepChar == Key)
m_RepChar = null;
}
if (m_RepChar != null && m_RepChar == Key && m_Input.CurrentKeyboardState.IsKeyDown(Key))
{
DateTime Now = DateTime.Now;
TimeSpan DownFor = Now.Subtract(m_DownSince);
if (DownFor.CompareTo(TimeSpan.FromMilliseconds(m_TimeUntilRepInMillis)) > 0)
{
// Should repeat since the wait time is over now.
TimeSpan repeatSince = Now.Subtract(m_LastRep);
if (repeatSince.CompareTo(TimeSpan.FromMilliseconds(1000f / m_RepsPerSec)) > 0)
// Time for another key-stroke.
m_LastRep = Now;
}
}
}
Keys[] PressedKeys = m_Input.CurrentKeyboardState.GetPressedKeys();
//Are these keys being held down since the last update?
if (m_RepChar == Keys.Back && m_LastRep == DateTime.Now)
RemoveText();
if (m_RepChar == Keys.Up && m_LastRep == DateTime.Now)
{
if(m_MultiLine)
MoveCursorUp();
}
if (m_RepChar == Keys.Down && m_LastRep == DateTime.Now)
{
if(m_MultiLine)
MoveCursorDown();
}
foreach (Keys K in PressedKeys)
{
if (m_Input.IsNewPress(K))
{
switch(K)
{
case Keys.Up:
if (m_RepChar != Keys.Up || m_LastRep != DateTime.Now)
{
if(m_MultiLine)
MoveCursorUp();
}
break;
case Keys.Down:
if (m_RepChar != Keys.Down || m_LastRep != DateTime.Now)
{
if (m_MultiLine)
MoveCursorDown();
}
break;
case Keys.Left:
if (!m_MultiLine)
MoveCursorLeft();
break;
case Keys.Right:
if (!m_MultiLine)
MoveCursorRight();
break;
case Keys.Back:
if (m_RepChar != Keys.Back || m_LastRep != DateTime.Now)
RemoveText();
break;
case Keys.LeftShift:
m_CapitalLetters = true;
break;
case Keys.RightShift:
m_CapitalLetters = true;
break;
case Keys.Enter:
AddNewline();
break;
}
}
}
base.Update(gameTime);
}
/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle((int)m_TextEditPosition.X,
(int)m_TextEditPosition.Y, (int)m_TextEditSize.X, (int)m_TextEditSize.Y);
Vector2 Position = m_TextPosition;
spriteBatch.Begin(SpriteSortMode.BackToFront);
if(m_Cursor.Visible)
spriteBatch.DrawString(m_Font, m_Cursor.Symbol, m_Cursor.Position, Color.White);
// TODO: Add your drawing code here
foreach (string Str in TextWithLBreaks.Split("\n".ToCharArray()))
{
spriteBatch.DrawString(m_Font, Str, Position, Color.White);
Position.Y += CapitalCharacterHeight;
}
spriteBatch.End();
base.Draw(gameTime);
}
}
}
UpdateCharacterPositions() and UpdateHitboxes() seems to work perfectly. That is, UNTIL the text has been scrolled up:
/// <summary>
/// Adds a newline to m_Text, and updates the cursor.
/// </summary>
private void AddNewline()
{
m_Text.Add("\n");
m_Cursor.CharacterIndex++;
m_Cursor.Position.X = m_TextEditPosition.X;
m_Cursor.LineIndex++;
//Scroll the text up if it's gone beyond the borders of the control.
if ((m_TextEditPosition.Y - TextSize().Y) < (m_TextEditPosition.Y - m_TextEditSize.Y))
{
m_TextPosition.Y -= CapitalCharacterHeight;
m_CanMoveCursorDown = false;
m_UpdateCharPositions = true;
}
if (m_CanMoveCursorDown)
m_Cursor.Position.Y += CapitalCharacterHeight;
m_NumLinesInText++;
}
I'm guessing that I need to take m_TextPosition.Y into consideration here:
foreach (Rectangle Hitbox in m_HitBoxes)
{
if (Hitbox.Contains(new Vector2(m_Input.MousePosition.X, m_Input.MousePosition.Y)) &&
m_Input.IsNewPress(MouseButtons.LeftButton))
{
m_Cursor.Position = new Vector2(Hitbox.X, Hitbox.Y);
int CharIndex = m_CharacterPositions.FirstOrDefault(x => x.Value == m_Cursor.Position).Key;
if (CharIndex != -1)
m_Cursor.CharacterIndex = CharIndex;
}
}
But I'm not entirely sure what to do. I tried both multiplying and adding m_TextPosition.Y to m_Input.MousePosition.Y and Hitbox.Y, but none of them seemed to work.
Please help!