Clicking text after it's been scrolled

Started by
0 comments, last by MatsK 5 years, 10 months ago

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!

This topic is closed to new replies.

Advertisement