I'm programmatically adding buttons to a container, but when the text in the button is too long, it stretches the button and the container with it.

The "Text Overrun Behavior" and "Clip Text" options both fix this, but do so by just hiding the rest of the text.
I tried adding a Label with autowrap inside of the button, but that creates even more issues: I couldn't find a way to make buttons resize to fit their children (instead the label is always overflowing), the Label theme will be used instead of the Button theme...

Is there a way to make the text autowrap and stretch the button vertically, like when you use text with newlines?
I guess I could write a function to break up the text into lines, but that sounds like it'd be really complicated since I'd have to take everything from the button width to the font family and font size into account :-(

  • award replied to this.
  • It's faaar from ideal, but here's what I threw together:

    public partial class ButtonMultiline : Button
    {
    	public void SetText(string text)
        {
    		float buttonSize = MainScene.DialogueOptions.GetRect().Size.X;
    		Text = text;
    
    		float textSize = GetThemeFont("font").GetStringSize(Text, HorizontalAlignment.Left, -1, GetThemeFontSize("font_size")).X;
    
    		if (buttonSize > textSize) return;
    
    		int lastspace = text.LastIndexOf(' ', (int)(text.Length*buttonSize/textSize));
    		text = text.Remove(lastspace, 1).Insert(lastspace, "\n");
    		Text = text;
        }
    }

    It's just for 2 lines and doesn't take button styles/padding into account, but I'll probably leave it alone for now and come back to it when I need it again

    I don't see a way to do that with a Button, but I could be wrong.

    You could use a different Control such as a Label or RichTextLabel, and have it respond to clicks like a Button.

      DaveTheCoder It crossed my mind too, but then I'd have to manually add all the button-related stuff like themes on hover, on click, focus and navigation with "ui_up/down" and "ui_select", et cetera. I need a button, not a clickable label :-/

      I'll see if I can put a label inside the button and set its size in code, I couldn't get it to work earlier but it was the closest I got to making it work at all.

      housatic I guess I could write a function to break up the text into lines, but that sounds like it'd be really complicated since I'd have to take everything from the button width to the font family and font size into account :-(

      I think it's not actually that bad. There is a helper function for it.

      https://docs.godotengine.org/en/stable/classes/class_font.html#class-font-method-get-string-size

      This should do all of the work for the text, so all you need is the display width for the button, which should just be its width - text margins I think

        award OH wow, I had no idea that function existed. I can't believe I went digging through the autowrap source code but didn't look through the font functions hahaha. Thanks!

        award Ok, get_string_size works great but I'm struggling with getting the button width, or getting the size of any of my Control nodes for that matter.
        None of them have set sizes, instead they're in containers with the Fill flags so they change dynamically when I resize the parent.

        I'm trying every vaguely size-related function/property from the docs but I can't find it 😬 In Remote I can see the display size so clearly it's saved somewhere

        // returns only the minimum content width (81x27 px), not the full width:
        GD.Print("S:" + control.Size);
        GD.Print("S:" + control.GetRect().Size);
        GD.Print("S:" + control.GetGlobalRect().Size);
        // returns 0 (I'm guessing this would only return something if I had set anchors, and not a fill flag):
        GD.Print("S:" + control.OffsetLeft);
        GD.Print("S:" + control.OffsetRight);
        GD.Print("S:" + control.AnchorLeft);
        GD.Print("S:" + control.AnchorRight);
        GD.Print("S:" + control.PivotOffset);

        Or do I need to calculate it myself from (the parent container's size) - (separation * number of children) - (sizes of all the other children that don't have fill flags)?

        • xyz replied to this.

          Hmm there has to be SOME point at which the control knows its real, exact dimensions, otherwise the engine couldn't draw it. I'll try looking through the cpp file later when I have a chance.

          housatic control.Size should return the same numbers you see in the inspector because it queries that exact property. Are you sure layout is not affecting it at runtime?

            xyz Huh, okay, if I call the function anywhere during _Ready I get the minimum content size, but anywhere after that gives me the right value, even though I'm not changing the layout at all in code. Are layouts only calculated(?) after _Ready?

            • xyz replied to this.

              housatic Are layouts only calculated(?) after _Ready

              Depends where that _Ready() is in the scene tree.

                xyz At the root of the scene... I'm guessing in that case the answer is yes

                It's faaar from ideal, but here's what I threw together:

                public partial class ButtonMultiline : Button
                {
                	public void SetText(string text)
                    {
                		float buttonSize = MainScene.DialogueOptions.GetRect().Size.X;
                		Text = text;
                
                		float textSize = GetThemeFont("font").GetStringSize(Text, HorizontalAlignment.Left, -1, GetThemeFontSize("font_size")).X;
                
                		if (buttonSize > textSize) return;
                
                		int lastspace = text.LastIndexOf(' ', (int)(text.Length*buttonSize/textSize));
                		text = text.Remove(lastspace, 1).Insert(lastspace, "\n");
                		Text = text;
                    }
                }

                It's just for 2 lines and doesn't take button styles/padding into account, but I'll probably leave it alone for now and come back to it when I need it again

                Just if you're curious, I looked into what the engine does for Label.

                BitField<TextServer::TextOverrunFlag> overrun_flags = TextServer::OVERRUN_NO_TRIM;
                switch (overrun_behavior) {
                	case TextServer::OVERRUN_TRIM_WORD_ELLIPSIS:
                		overrun_flags.set_flag(TextServer::OVERRUN_TRIM);
                		overrun_flags.set_flag(TextServer::OVERRUN_TRIM_WORD_ONLY);
                		overrun_flags.set_flag(TextServer::OVERRUN_ADD_ELLIPSIS);
                		break;
                	case TextServer::OVERRUN_TRIM_ELLIPSIS:
                		overrun_flags.set_flag(TextServer::OVERRUN_TRIM);
                		overrun_flags.set_flag(TextServer::OVERRUN_ADD_ELLIPSIS);
                		break;
                	case TextServer::OVERRUN_TRIM_WORD:
                		overrun_flags.set_flag(TextServer::OVERRUN_TRIM);
                		overrun_flags.set_flag(TextServer::OVERRUN_TRIM_WORD_ONLY);
                		break;
                	case TextServer::OVERRUN_TRIM_CHAR:
                		overrun_flags.set_flag(TextServer::OVERRUN_TRIM);
                		break;
                	case TextServer::OVERRUN_NO_TRIMMING:
                		break;
                }
                
                // Fill after min_size calculation.
                
                BitField<TextServer::JustificationFlag> line_jst_flags = jst_flags;
                if (!tab_stops.is_empty()) {
                	line_jst_flags.set_flag(TextServer::JUSTIFICATION_AFTER_LAST_TAB);
                }
                if (autowrap_mode != TextServer::AUTOWRAP_OFF) {
                	int visible_lines = get_visible_line_count();
                	bool lines_hidden = visible_lines > 0 && visible_lines < lines_rid.size();
                	if (lines_hidden) {
                		overrun_flags.set_flag(TextServer::OVERRUN_ENFORCE_ELLIPSIS);
                	}
                	if (horizontal_alignment == HORIZONTAL_ALIGNMENT_FILL) {
                		int jst_to_line = visible_lines;
                		if (lines_rid.size() == 1 && line_jst_flags.has_flag(TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE)) {
                			jst_to_line = lines_rid.size();
                		} else {
                			if (line_jst_flags.has_flag(TextServer::JUSTIFICATION_SKIP_LAST_LINE)) {
                				jst_to_line = visible_lines - 1;
                			}
                			if (line_jst_flags.has_flag(TextServer::JUSTIFICATION_SKIP_LAST_LINE_WITH_VISIBLE_CHARS)) {
                				for (int i = visible_lines - 1; i >= 0; i--) {
                					if (TS->shaped_text_has_visible_chars(lines_rid[i])) {
                						jst_to_line = i;
                						break;
                					}
                				}
                			}
                		}
                		for (int i = 0; i < lines_rid.size(); i++) {
                			if (i < jst_to_line) {
                				TS->shaped_text_fit_to_width(lines_rid[i], width, line_jst_flags);
                			} else if (i == (visible_lines - 1)) {
                				TS->shaped_text_overrun_trim_to_width(lines_rid[i], width, overrun_flags);
                			}
                		}
                	} else if (lines_hidden) {
                		TS->shaped_text_overrun_trim_to_width(lines_rid[visible_lines - 1], width, overrun_flags);
                	}

                It looks like the TextServer is actually used to handle the line-breaking. The same could probably be done for Buttons. Probably best to just stick with what you need for right now though 🙂

                8 months later