Skip to content

Source: Get Character At Coord

Hapaxia edited this page Nov 7, 2022 · 2 revisions

Introduction

SFML's texts are considered a single drawable object so interaction with them is usually based on their boundary boxes.

Get Character At Coord informs you of which character in a text object is at the given co-ordinate.

Get Character At Coord is a free-function that can be added to any code.

There are two versions.
The standard version works with all text objects (including multi-line texts) and a compact one that will only work correctly with single-line texts.
The Single-line version is a shorter version for when you know you won't be using multi-line texts. However, the standard version will do multi-line as well as single-line texts.

Code

Standard Version

std::size_t getCharacterIndexAtCoord(const sf::Text& text, const sf::Vector2f coord)
{
    std::size_t indexOfCharacterUnderCoord{ text.getString().getSize() };
    if (text.getGlobalBounds().contains(coord))
    {
        const std::size_t size{ text.getString().getSize() };
        std::size_t startI{ 0u };
        std::size_t nextStartI{ 0u };
        float y{ text.findCharacterPos(0u).y };
        for (std::size_t i{ 0u }; i <= size; ++i)
        {
            const float characterPosY{ text.findCharacterPos(i).y };
            if (characterPosY > coord.y)
            {
                startI = nextStartI;
                break;
            }
            if ((characterPosY > y) || ((i + 1u) == text.getString().getSize()))
            {
                y = characterPosY;
                startI = nextStartI;
                nextStartI = i;
            }
        }
        y = text.findCharacterPos(startI).y;
        for (std::size_t i{ startI }; i <= size; ++i)
        {
            const sf::Vector2f characterPos{ text.findCharacterPos(i) };
            if ((characterPos.y > y) || (characterPos.x > coord.x))
                break;
            indexOfCharacterUnderCoord = i;
        }
    }
    return indexOfCharacterUnderCoord;
}

Compact Version

Note that this compact version is named slightly different (has "SingleLine" appended) so that both can be used if required.

std::size_t getCharacterIndexAtCoordSingleLine(const sf::Text& text, const sf::Vector2f coord)
{
    std::size_t indexOfCharacterUnderCoord{ text.getString().getSize() };
    if (text.getGlobalBounds().contains(coord))
    {
        const std::size_t size{ text.getString().getSize() };
        for (std::size_t i{ 0u }; i < size; ++i)
        {
            if (text.findCharacterPos(i).x > coord.x)
                break;
            indexOfCharacterUnderCoord = i;
        }
    }
    return indexOfCharacterUnderCoord;
}

Usage

Inclusion

Either (or both) function can be copied and pasted into your code and used directly. You can also create a separate header file for them and include them when needed.

You may need these includes (before the function) if you haven't already included them:
#include <cstddef>
#include <SFML/Graphics/Text.hpp>
#include <SFML/System/Vector2.hpp>

Function

It has very simple usage; you simply provide it with the text object (sf::Text) and a co-ordinate (sf::Vector2f) and the function returns the index of the string used in the text object.

So, presuming some setup like this:

sf::Text text;
sf::Vector2f coord;

For multi-line text (standard version): std::size_t characterIndex{ getCharacterIndexAtCoord(text, coord); }

For single-line text (compact version): std::size_t characterIndex{ getCharacterIndexAtCoordSingleLine(text, coord); }

Remember that you can simply use the standard (multi-line) version for single-line text too.

characterIndex then has the index of the character (starting from zero for the first character) in the text object's string that appears at the co-ordinate.

If characterIndex (the returned value) is equal to the length of the string, the co-ordinate is not over any character and should be considered as "no character". Attempting to get a character from the string using an invalid index such as "length of string" is a mistake.

License

This code is from https://github.com/Hapaxia/SfmlSnippets/tree/master/GetCharacterAtCoord and provided under the zlib license (the same license type as SFML).

It is from the GitHub repository SfmlSnippets by Hapaxia.

Example

Here is a full example, showing two white text objects - one single-line and one multi-line (including a tab character). The individual character that is below the mouse cursor is turned red with a white outline. This example was used to create this screenshot (click for full size):
example screenshot

////////////////////////////////////////////////////////////////
//
// The MIT License (MIT)
//
// Copyright (c) 2022 M.J.Silk
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////
//
//
//       ------------
//       INTRODUCTION
//       ------------
//
//   Displays a single line of text as well as a multi-line text object.
//   Whichever character the mouse hovering over, is shown (as well as its index in the text string) and the character is also visually highlighted.
//
//
//       --------
//       CONTROLS
//       --------
//
//   MOUSE           hover cursor over a text character
//   ESC             quit
//
//
//        ----
//        NOTE
//        ----
//
//    If the window is too large (1600x900) for your resolution, you can uncomment the following define line to halve the window size (to 800x450) and scale the texts to match: #define HALVE_WINDOW_SIZE
//
//
////////////////////////////////////////////////////////////////



#include <iostream>
#include <functional>
#include <SFML/Graphics.hpp>

#include "GetCharacterAtCoord.hpp"



//#define HALVE_WINDOW_SIZE



int main()
{
	sf::Vector2u windowSize{ 1600u, 900u };
#ifdef HALVE_WINDOW_SIZE
	windowSize /= 2u;
#endif // HALVE_WINDOW_SIZE



	sf::Font font;
	if (!font.loadFromFile("resources/fonts/arial.ttf"))
	{
		std::cerr << "ERROR LOADING FONT" << std::endl;
		return EXIT_FAILURE;
	}



	auto setTextOriginToCenter = [](sf::Text& text)
	{
		const sf::FloatRect textLocalBounds{ text.getLocalBounds() };
		text.setOrigin(textLocalBounds.left + (textLocalBounds.width / 2.f), textLocalBounds.top + (textLocalBounds.height / 2.f));
	};

	sf::Text text;
	text.setFont(font);
	text.setCharacterSize(64u);
	text.setFillColor(sf::Color::White);
	text.setString("ABCDEF\nGHIJKLM\nNOPQRST\n\tUVWXYZ\n12345\n67890");
	setTextOriginToCenter(text);
	text.setPosition(sf::Vector2f(windowSize.x * 0.5f, windowSize.y * 0.6f));

	sf::Text textSingleLine = text;
	textSingleLine.setString("ABCDEFGHIJKLMNOPQRST\tUVWXYZ1234567890");
	setTextOriginToCenter(textSingleLine);
	textSingleLine.setPosition(sf::Vector2f(windowSize.x * 0.5f, windowSize.y * 0.25f));

	sf::Text textCharacterHighlight = text;
	textCharacterHighlight.setString("");
	textCharacterHighlight.setOrigin({ 0.f, 0.f });
	textCharacterHighlight.setFillColor(sf::Color::Red);
	textCharacterHighlight.setOutlineColor(sf::Color::White);
	textCharacterHighlight.setOutlineThickness(2.f);

	sf::Text feedbackText;
	feedbackText.setFont(font);
	feedbackText.setCharacterSize(36u);
	feedbackText.setFillColor(sf::Color::Green);
	feedbackText.setPosition({ 5.f, 5.f });

#ifdef HALVE_WINDOW_SIZE
	text.setScale(0.5f, 0.5f);
	textSingleLine.setScale(0.5f, 0.5f);
	textCharacterHighlight.setScale(0.5f, 0.5f);
	feedbackText.setScale(0.5f, 0.5f);
#endif // HALVE_WINDOW_SIZE



	sf::RenderWindow window(sf::VideoMode(windowSize.x, windowSize.y), "");

	while (window.isOpen())
	{
		// EVENTS

		sf::Event event;
		while (window.pollEvent(event))
		{
			if ((event.type == sf::Event::Closed) || ((event.type == sf::Event::KeyPressed) && (event.key.code == sf::Keyboard::Escape)))
				window.close();
		}





		// UPDATE

		const sf::Vector2f mouseCoord{ window.mapPixelToCoords(sf::Mouse::getPosition(window)) };

		bool isMouseInsideText{ false };
		bool isMouseInsideSingleLineText{ false };
		char characterUnderMouse{ '\n' };
		char singleLineCharacterUnderMouse{ '\n' };
		std::size_t indexOfCharacterUnderMouse{ 0u };
		std::size_t indexOfSingleLineCharacterUnderMouse{ 0u };

		auto processTextUnderMouse = [mouseCoord, &textCharacterHighlight](bool& isMouseInside, sf::Text& text, char& c, std::size_t& i)
		{
			isMouseInside = text.getGlobalBounds().contains(mouseCoord);
			i = getCharacterIndexAtCoord(text, mouseCoord);
			if (i < text.getString().getSize())
			{
				c = text.getString()[i];
				textCharacterHighlight.setString(c);
				textCharacterHighlight.setPosition(text.findCharacterPos(i));
			}
		};
		textCharacterHighlight.setString("");
		processTextUnderMouse(isMouseInsideText, text, characterUnderMouse, indexOfCharacterUnderMouse);
		processTextUnderMouse(isMouseInsideSingleLineText, textSingleLine, singleLineCharacterUnderMouse, indexOfSingleLineCharacterUnderMouse);

		std::string feedbackString;
		auto composeFeedbackString = [](char c, std::size_t i) { return ((c == '\n') ? "" : "Character Under Mouse: " + ((c == '\t') ? "[TAB]" : "\'" + std::string(1u, c) + "\'") + " (index: " + std::to_string(i) + ")"); };
		if (isMouseInsideText)
			feedbackString = composeFeedbackString(characterUnderMouse, indexOfCharacterUnderMouse);
		else if (isMouseInsideSingleLineText)
			feedbackString = composeFeedbackString(singleLineCharacterUnderMouse, indexOfSingleLineCharacterUnderMouse);
		feedbackText.setString(feedbackString);





		// RENDER

		window.clear();
		window.draw(textSingleLine);
		window.draw(text);
		if (isMouseInsideText || isMouseInsideSingleLineText)
			window.draw(textCharacterHighlight);
		window.draw(feedbackText);
		window.display();
	}
}

This example is also available on the GitHub repository.

The instructions for this example are contained in comments near the top of the example code.

The example code presumes that the functions are in a separate header file (as shows on the GitHub repository) and that file is included. You can, however, simply copy and paste the functions where the inclusion is.

As you can see, this example code uses both a single-line text object and a multi-line text object. It therefore uses both functions - the standard one for the multi-line text and the compact one for the single-line text - to show that they both be used in the same code, but it is not required to use the compact version; the standard version will work on the single-line text as well.

As visible in the code, the example is licensed using the MIT license.


Written by Hapaxia (Github | Twitter | SFML forum)

Clone this wiki locally