godotgdscript

How to create a scalable multi-line button in Godot?


A Button in Godot can only hold a single line of text. I can overcome this limitation by placing RichTextLabel node inside the button.

Now the button can contain more lines of text, but its height doesn't change automatically when more lines are needed. Instead the text just overflows:

screenshot of button

Of course I can manually resize the button to be higher, but I'd like this to happen automatically depending on the amount of text inside. Specifically, I'm generating a list of these buttons programmatically and showing inside a HBoxContainer, with some buttons having longer and other shorter text.

Is there a way to achieve this with Godot layout tools?


Solution

  • Since the Button is in a Container, it is in control of its rect_size. The best we can do is specify a rect_min_size. There is no layout preset to have a Control depend on children Control. So, to answer the question as posted: No, we cannot achieve this with Godot layout tools. We need some scripting.


    We need to set the rect_min_size for the Button depending on the RichTextLabel. We can ask it for the height of its content with get_content_height. Which also means we need to set the width beforehand. However, it will not update right away when we set the text (we are going to use yield).


    Apparently you don't want the Container to control the height of the Button. If that is the case, I think you can remove all the flags from size_flags_vertical.

    About the width, since as I was explaining before we need to set the width to get the height… I suggest you let the Container expand the width of the Button as much a posible. Which mean setting both the Fill and Expand flags on size_flags_horizontal.

    Then, with the RichTextLabel properly set to take as much width of the parent Button as possible, you can read it height, and use it to set the height of the rect_min_size of the Button.


    One more thing: you want to set the mouse filter of the RichTextLabel to Ignore or Pass, or it will prevent pressing the Button.


    This is the code I came up with:

    var b := Button.new()
    b.size_flags_vertical = 0
    b.size_flags_horizontal = SIZE_EXPAND_FILL
    add_child(b)
    
    var l := RichTextLabel.new()
    l.mouse_filter = Control.MOUSE_FILTER_IGNORE
    l.set_anchors_and_margins_preset(Control.PRESET_WIDE)
    l.text = "Some\nMultiline\nText"
    b.add_child(l)
    
    yield(get_tree(), "idle_frame")
    b.rect_min_size.y = l.get_content_height()
    

    I'd like this to happen automatically depending on the amount of text inside

    Sadly changing the text does not resize, nor change the minimum size of the RichTextLabel. And RichTextLabel does not have a "text changed" signal. Nor "bbcode text changed" signal. Furthermore, it might not be feasible to intercept these properties (see append_bbcode et.al). It is proabaly easier to do with a regular Label.

    Anyway, what I'm going to suggest for this is to make a Control that wraps the RichTextLabel, offers whatever interface you actually need, and in any method where you change the text, afterwards, you do the equivalent of this:

    yield(get_tree(), "idle_frame")
    b.rect_min_size.y = l.get_content_height()