Label or RichTextLabel auto width?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By shianiawhite

Is there a way to add text to a Label or RIchTextLabel, then get the text width to determine the correct width to automatically fit that text into the Label (and this width could also be used for sizing other nodes)? Thank you!

:bust_in_silhouette: Reply From: Poika Pilvimaa

I know this is a very old question, but it pops up high in searches and nobody had replied to it, and I felt bad for everyone affected. This is still very much a problem in Godot.

Here’s my solution written in C#.

I haven’t tested it super heavily yet, so keep an eye out for bugs.

Also, this might not be a generic “one size fits all” solution, for example I’m ignoring bitmap fonts entirely.

Have fun.

using System;
using System.Collections.Generic;
using Godot;

namespace Haikuchatclient {

public static class PixelTools {

    // Find out message height in pixels.
    // **********************************
    // Can be used to set surrounding elements of Labels etc. to a proper height.
    public static int GetPixelHeightForText(string text, DynamicFont dynamicFont, int surroundingElementWidth) {

        // Get line height in pixels, ie. font height. It's always standard AFAIK.
        var fontHeight = dynamicFont.GetHeight();

        // Split the text into an array using spaces as delimiters - and include the spaces.
        var splitText = SplitAndKeepDelimiters(text, " ");

        // Calculate how many lines of text there will be.
        var lineCount = CalculateLines(splitText, dynamicFont, surroundingElementWidth);
        
        // Calculate the pixel height needed to fit in the text.
        var pixelHeight = lineCount * fontHeight;// + lineSpacing;

        // Account for the outline surrounding the font.
        pixelHeight += (dynamicFont.OutlineSize * lineCount);
        
        // Return the result.
        return (int) pixelHeight;
    }

    
    /// <summary>
    /// Splits the given string into a list of substrings, while outputting the splitting
    /// delimiters (each in its own string) as well. It's just like String.Split() except
    /// the delimiters are preserved. No empty strings are output.</summary>
    /// <param name="s">String to parse. Can be null or empty.</param>
    /// <param name="delimiters">The delimiting strings. Can be an empty array.</param>
    /// <returns></returns>
    public static IList<string> SplitAndKeepDelimiters(string s, params string[] delimiters) {

        var parts = new List<string>() { s };

        if (!string.IsNullOrEmpty(s)) {

            foreach (string delimiter in delimiters) { // Delimiter counter.
				
                for (int i = 0; i < parts.Count; i++) { // Part counter.
				
                    int index = parts[i].IndexOf(delimiter, StringComparison.Ordinal);

                    if (index > -1 && parts[i].Length > index + 1) {
                        string leftPart = parts[i].Substring(0, index + delimiter.Length);
                        string rightPart = parts[i].Substring(index + delimiter.Length);
                        parts[i] = leftPart;
                        parts.Insert(i + 1, rightPart);
                    }
                }
            }
        } 
        return parts;
    }

    
    private static int CalculateLines(IList<string> splitText, DynamicFont dynamicFont, int surroundingElementWidth) {

        // Keeps track how much pixels we have left before we need to wrap to a next line.
        var widthLeftUntilNextLine = surroundingElementWidth;

        // Get last index position so we can refer to it.
        var lastIndex = splitText.Count - 1;

        // We need to keep track of the current index so we know when we hit the last one - so we can add last line.
        var indexCounter = 0;

        // We start from line 1, because there is always at least one line.
        var totalLines = 1;

        // Iterate through the split message to find out the amount of lines the message will split to.
        foreach (var word in splitText) {

            // Let's find out how many pixels there are in this word.
            var pixelsInWord = GetWordPixelWidth(word, dynamicFont);
            
            // If the amount of pixels is more than one line can handle we go to the next line.
            if (pixelsInWord > widthLeftUntilNextLine) {
                totalLines += 1;

                // TODO: Code will bug out if the word is too long. One way to handle it is to do manual line breaks. 
                // Subtract this word's width from the next line because this word didn't fit on last line.
                widthLeftUntilNextLine = surroundingElementWidth - pixelsInWord;

                // We can handle some more words. No line break yet.
            } else if (pixelsInWord == widthLeftUntilNextLine) {
                // Prevent border case where we don't need the extra line.
                if (indexCounter != lastIndex) {
                    totalLines += 1;
                }

                widthLeftUntilNextLine = surroundingElementWidth;
            } else {
                // Reduce the word's length from the pixels left for this line.
                widthLeftUntilNextLine -= pixelsInWord;
            }

            // Increase the index for the next round.
            indexCounter += 1;
        }

        return totalLines;
    }

    
    // Return the total width of a string in pixels.
    // TODO: Could this be turned into an extension method?
    public static int GetWordPixelWidth(string word, DynamicFont dynamicFont) {
        return (int) dynamicFont.GetStringSize(word).x;
    }
    
}

}

Also, this might not be a generic “one size fits all” solution, for example I’m ignoring bitmap fonts entirely (come on, bitmap fonts are so 1960’s… who uses them anymore, seriously).

Bitmap fonts are still popular in pixel art games, as these give you much more creative freedom compared to DynamicFont.

Calinou | 2020-06-14 13:03