Search code examples
c++sdl-2sdl-ttf

TTF_SizeText() not returning correct values for 'i' 'j' and '1' (so far discovered) in SDL2, C++


I have a textbox class that works nicely with wider characters such as a,b,c... but with characters like 'f' and 'l' it seems to incorrectly get the sizing of those characters, yet correctly get the sizing of the others? Here is the code for the 'highlighting' of the text for the textbox class, its a bit long ill fix that up later, but should documented enough to understand easily.

void Textbox::Highlight_Text(SDL_Renderer *renderer)
{
    if (clickedOn == true){


        int currentCharacterWidth = 0;
        int currentCharacterHeight = 0;
        int totalSize = 0;
        SDL_Rect currentCharacterRect;
        string currentCharacter, tempText;



        if (highlightedCharacters.size() >= 1){ ///To make sure only 1 thing is highlighted, in conjunction with next part
            highlighted = true;
        }

        if (highlighted == true){   /// if a part is highlighted, and is left highlighted, next time clicked, remove the highlighting and redo it
            if (EVENTS.mouseClicked == false){
                resetHighlightingNextClick = true;
            }
        }

        if (resetHighlightingNextClick == true){
            if (highlighted == true){
                if (EVENTS.mouseClicked == true){       ///actually remove the highlighting
                    highlightedCharacters.clear();
                    indexOfCharactersHighlighted.clear();
                    highlighted = false;
                    resetHighlightingNextClick = false;
                }
            }
        }



        for (int i=0; i < textboxText.Get_Text().size(); i++){
            currentCharacter =  textboxText.Get_Text()[i];
            TTF_SizeText(textboxText.fonts[textboxText.fontIndex], currentCharacter.c_str(), &currentCharacterWidth, &currentCharacterHeight);

            ///the totalSize added to rectangle is not making it wider, its adjusting its x value offset
            currentCharacterRect = {textboxText.x + totalSize, textboxText.y + int(textboxText.textSize*0.1), currentCharacterWidth, currentCharacterHeight};
            totalSize += currentCharacterWidth; ///"current" size of text in loop to get x value of specific character clicked on

            ///If mouse is touching any of the characters in the text
            if ( SDL_PointInRect(&EVENTS.mousePos, &currentCharacterRect) ){
                EVENTS.Change_Cursor(SDL_SYSTEM_CURSOR_IBEAM);

                if (EVENTS.mouseClicked == true){   ///Clicking on the text to highlight
                    if (In_Array(highlightedCharacters, currentCharacterRect.x) == false  ){
                        highlightedCharacters.push_back(currentCharacterRect);  ///If there is no duplicates
                        indexOfCharactersHighlighted.push_back(i); ///Get index of text being highlighted, its always in order too

                    }

                    if (  currentCharacterRect.x != highlightedCharacters[highlightedCharacters.size()-1].x){ ///So they don't stack up highlights, ie, you can remove them
                        /// If the mouse is not highlighting the last one, say second last on the right for example, delete the one in front of it (last one)
                        ///Like when highlighting text with mouse, it adapts to how you move it, so it unhighlights text not being highlighted
                        highlightedCharacters.pop_back();
                        indexOfCharactersHighlighted.pop_back();

                    }
                }

            }

        }///End for loop




        if (highlighted == true ){
            if (EVENTS.backspacePressed == true || EVENTS.currentKey != ""){
                tempText = textboxText.Get_Text();

                ///remove highlighted characters
                if (indexOfCharactersHighlighted.size() != 0){
                    ///the range of values highlighted will always be in a sorted order
                    tempText.erase( Min(indexOfCharactersHighlighted)  , Max(indexOfCharactersHighlighted)-Min(indexOfCharactersHighlighted)+1  );  ///erase the range of values highlighted
                    textboxText.Change_Text(renderer, tempText);

                    ///once removed text, clear every highlighted related thing
                    highlightedCharacters.clear();
                    indexOfCharactersHighlighted.clear();
                    highlighted = false;
                    resetHighlightingNextClick = false;

                    EVENTS.backspacePressed = false;
                    EVENTS.currentKey = "";
                }

            }
        }



    }   ///End if for clicked on



    ///fit with scrolling offsets
    if (EVENTS.scrolled == true){
        for (int p=0; p < highlightedCharacters.size(); p++){
            highlightedCharacters[p].y += EVENTS.scrollVal;
        }
    }




    ///Drawing the highlighted text
    if (highlighted == true   &&   clickedOn == true){
        SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
        SDL_SetRenderDrawColor(renderer, 55,60,65, 75);
        for (int j=0; j < highlightedCharacters.size(); j++){
            SDL_RenderFillRect(renderer, &highlightedCharacters[j]);
        }
        SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
    }


    ///when clicked off textbox, clear everything/highlighting
    if (clickedOn == false){
        highlightedCharacters.clear();
        indexOfCharactersHighlighted.clear();
        highlighted = false;
    }



}

For reference in the font passed in, here is how i obtain it in the text class

    fontIndex = textSize-lowestFontSize  -1;

    ///One time setups
    if (numOfInstances == 1){
        try{
            TTF_Init();
            //cout << "Initialised ttf" << endl;
        }
        catch (exception &err){
            cout << "Could not initialise ttf for text \"" << text << "\". Error from SDL is: " << TTF_GetError() << ". Error from C++ is: " << err.what() << endl;
        }

        for (int i=lowestFontSize; i <= highestFontSize; i++){
            TTF_Font *currentFont = TTF_OpenFont(fontType.c_str(), i);
            if (!currentFont){
                cout << "Error with font in text \"" << txt << "\" Error is: " << SDL_GetError() << endl;
            }

            fonts.push_back(currentFont);
        }

    }

and so if i pass in, say 25 as my text size, i have my lowestFontSize = 10 and highestFontSize = 100, so the index i need for size 25 would be (25-10 -1 = 14), as indexing begins at 0, which is that first line before i create my static vector of fonts in the text class. Here is a snippet of what i'm trying to explain:

enter image description here

This is clearly working properly.

enter image description here

But now, it is completely inaccurate. If i select a random character towards the end of the text, it is not correctly highlighted, only from the beginning the first one looks pretty much perfect, but then it seems as if the inaccuracy is compounded, hence making the total grey highlighting much wider than it is supposed to be.


Solution

  • The problem with your method is that individual character widths are meaningless. The renderer adjusts them depending on context (their neighbours in the rendered string). So the width of i in the rendered string bit is not necessarily the same as the width of i in the rendered string fil.

    The method to find text selection coordinates needs to take context into account.

    Say we have the width of the three strings:

    • prefixWidth is the size of the prefix (the original line of text up to but not including the selection)
    • selWidth is the width of the selection itself
    • totalWidth is the width of the prefix and the selection concatenated

    (we don't care about the portion after the selection). The first two widths will be close to the third, but will not add up because of the kerning between the last character of the prefix and the first character of the selection. The difference is the correction you need to apply to the X coordinate of the rendered selection. So we need to start rendering selection at the X coordinate which is not prefixWidth, but

    prefixWidth + (totalWidth - (prefixWidth + selWidth))
    

    which is the same as

    totalWidth - selWidth
    

    so you don't really need to calculate prefixWidth at all.

    A full (semi-)working example program below. The arguments are (1) font file full path (2) font size (3) string to render (4) selection start (5) selection length. The selection is rendered on top of the original text, shifted 1 pixel down and right, so it is easy to see if there is any deviation.

    #include <SDL2/SDL.h>
    #include <SDL2/SDL_ttf.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <stdbool.h>
    
    char* substr(const char* src, int fromChar, int subLen)
    {
        if (subLen < 0 || fromChar < 0 || fromChar + subLen > (int)strlen(src))
        {
            fprintf (stderr, "invalid substring\n");
            exit (EXIT_FAILURE);
        }
    
        char* z = (char*)malloc(subLen);
        strncpy(z, src + fromChar, subLen);
        z[subLen] = '\0';
        return z;
    }
    
    void textExtent(TTF_Font* font, const char* text,
                    int fromChar, int subLen, int* w, int* h)
    {
        int l = strlen(text);
        if (subLen == -1) subLen = l;
        if (fromChar < 0 || subLen > l)
        {
            fprintf (stderr, "Bad text extent\n");
            exit (EXIT_FAILURE);
        }
    
        char* z = substr(text, fromChar, subLen);
    
        TTF_SizeUTF8(font, z, w, h);
    
        free(z);
    }
    
    int textWidth(TTF_Font* font, const char* text,
        int fromChar, int subLen)
    {
        int w, h;
        textExtent(font, text, fromChar, subLen, &w, &h);
        return w;
    };
    
    int main(int argc, char ** argv)
    {
        bool quit = false;
        SDL_Event event;
    
        SDL_Init(SDL_INIT_VIDEO);
        TTF_Init();
    
        if (argc != 6)
        {
            fprintf (stderr, "usage: %s font text from length\n", argv[0]);
            exit(EXIT_FAILURE);
        }
    
        const char* fontpath = argv[1];
        int fontSz = atoi(argv[2]);
        const char* txt = argv[3];
        int from = atoi(argv[4]);
        int len = atoi(argv[5]);
    
        int tsize = strlen(txt);
        if (from < 0 || from + len >= tsize)
        {
            fprintf (stderr, "Invalid text portion to highlight\n");
            exit (EXIT_FAILURE);
        }
    
        if (fontSz < 2 || fontSz > 300)
        {
            fprintf (stderr, "Invalid font size\n");
            exit (EXIT_FAILURE);
        }
    
        // open font to render with
    
        TTF_Font * font = TTF_OpenFont(fontpath, fontSz);
        if (!font)
        {
            fprintf (stderr, "Could not open font %s\n", fontpath);
            exit (EXIT_FAILURE);
        }
    
        // Query text size
        int textW, textH;
        textExtent(font, txt, 0, -1, &textW, &textH);
    
        SDL_Window * window = SDL_CreateWindow("SDL_ttf in SDL2",
            SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, textW, textH, 0);
    
        // Query selection coords
        //
        int selWidth = textWidth(font, txt, from, len);
        int totalWidth = textWidth(font, txt, 0, from+len);
    
        // Render portions of text
    
        SDL_Renderer * renderer = SDL_CreateRenderer(window, -1, 0);
        SDL_Color color = { 255, 128, 0, 0 };
        SDL_Surface * surface = TTF_RenderUTF8_Blended(font, txt, color);
        SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_BLEND);
        SDL_Texture * texture = SDL_CreateTextureFromSurface(renderer, surface);
        SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
    
        SDL_Rect dstrect = { 0, 0, textW, textH };
    
        SDL_Color color2 = { 0, 128, 255, 128 };
    
        char* s = substr(txt, from, len);
        SDL_Surface * surface2 = TTF_RenderUTF8_Blended(font, s, color2);
        free(s);
        SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_BLEND);
        SDL_Texture * texture2 = SDL_CreateTextureFromSurface(renderer, surface2);
        SDL_SetTextureAlphaMod(texture2, 128);
        SDL_SetTextureBlendMode(texture2, SDL_BLENDMODE_BLEND);
        SDL_Rect dstrect2 = {totalWidth - selWidth + 1, 1, selWidth, textH };
    
        while (!quit)
        {
            SDL_WaitEvent(&event);
    
            switch (event.type)
            {
                case SDL_QUIT:
                    quit = true;
                    break;
            }
    
            SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
            SDL_RenderClear(renderer);
    
            SDL_RenderCopy(renderer, texture, NULL, &dstrect);
            SDL_RenderCopy(renderer, texture2, NULL, &dstrect2);
    
            //Update screen
            SDL_RenderPresent(renderer);
    
        }
    
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_DestroyTexture(texture);
        SDL_FreeSurface(surface);
        TTF_CloseFont(font);
        TTF_Quit();
        SDL_Quit();
    
        return 0;
    }