Scrolling text log

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

I want to create a log for an interactive fiction-type project. It should display previous commands and responses, and should be scrollable once the size of the log exceeds the text window. I’ve had some success with using a rich text label node combined with a line edit node, and I’m able to log inputs, produce responses, and generally have the innards working as they should.

However, it doesn’t look quite right. I have the label situated directly above the line edit. What I want is for the text to appear directly above the line edit (that is, at the bottom of the text window), and for older commands to be pushed upwards as new commands are received, until they eventually disappear at the top of the text window, ideally with the option to scroll upwards to see the cut-off text.

The regular Label node supports text alignment, so I can glue the text to the bottom of the window, but it lacks scrolling, and using it would also entail fooling around with a lot of code in order to limit the number of lines based on window size and whatnot, which I would prefer to avoid if possible.

Any suggestions?

Edit: Examples for clarity.

This is what I want, the text appears at the bottom and older messages get pushed upwards:

This is what the RichTextEdit gives me. New messages still appear below the older ones, but the text initially appears at the top of the window:
enter image description here

I want the text window to be resizeable, so simply making a bunch of newlines at runtime won’t cut it, unfortunately.

:bust_in_silhouette: Reply From: Aristonaut

Not totally sure I follow your question, but you could make a reverse message scroller by just tracking the messages in a list and keeping that to a certain length like so:

extends RichTextLabel

func _process(delta):
	addMessage( "Hey Sexy" )

var messages = []
var max_messages = 100

func addMessage( text : String ):
	messages.append( text )
	if messages.size() > max_messages:
		messages.remove(0)
	
	text = arr_join( messages, "\n" )
	scroll_to_line( get_line_count() - 1 )

func arr_join(arr, separator = ""):
    var output = "";
    for s in arr:
        output += str(s) + separator
    output = output.left( output.length() - separator.length() )
    return output 

While it’s not the most performant thing ever, I doubt it will matter, given the use case. This heavy string construction will only be done when a new message is added, not every frame. If your interactive fiction gets more intensive, then you may want to figure out a faster solution. I wouldn’t optimize till it was a problem, though. Easy solution first.

Thank you for the response. I may have been unclear in my initial post, but reversing the sequence of commands isn’t actually the issue. What I can’t do is get the most recent input to always be displayed at the bottom of the text window rather than start at the top (see the edit above).

vivavovuve | 2019-12-31 11:39

Ah, I see what you mean now. Yeah, that is a little trickier. Looks like RichTextEdits don’t have a way to auto-expand their content at the moment. May be fixed in the future, then you can just have that auto-expand till it fills the empty space, then the scrollbar will show up when it has hit it’s bounds. Until then, you’ll have to stick with your solution, probably.

Aristonaut | 2020-01-08 03:39

:bust_in_silhouette: Reply From: Klagsam

I just tested this and I think there might be a simple solution.
My test layout was like this:

RichtextLabel
RichtextLabel
RichtextLabel
RichtextLabel
LineEdit

On the Label I set ‘scroll_active = true’ and ‘scroll_following = true’

Then I did write a carriage return in the first three lines of the Label. Now every Line I enter in the LineEdit is displayed at the bottom and old text is scolling up.

My code is as following with the text_entered signal being connected to the function:

func _on_LineEdit_text_entered(new_text):
$RichTextLabel.add_text(new_text)
$RichTextLabel.newline()
$LineEdit.text = ""

Thanks, but provided I’m understanding you correctly and by carrriage return you mean simply making a newline, this method won’t support multiple resolutions the way I want it to.

vivavovuve | 2019-12-31 11:37

You could compute the amount of new lines from the resolution and the font size, couldn’t you?

Klagsam | 2019-12-31 11:48

I could, yes. It’ll be a bit of work to get it looking okay and there’ll be a few niggles, but I suppose it’s my best bet. Cheers!

vivavovuve | 2019-12-31 12:54

:bust_in_silhouette: Reply From: vivavovuve

I did it the hard way in the end. Posting the working code for posterity, applied to the TextParser node in the following construction:
enter image description here

onready var font = $Label.get_font("normal_font")
onready var window_size = $Label.get_size()

var max_lines
var total_lines = 0
var scroll = false

func addPlaceholderSpaces():
    $Label.clear()
    var font_height = font.get_height()
    max_lines = window_size[1] / font_height
    print(max_lines)
    for line in max_lines:
	    $Label.newline()

func addText(text):
	if text != '':
		$Label.newline()
		$Label.add_text( text)
		addLines(text)
	$TextEdit.set_text('')
	if not scroll and total_lines >= max_lines:
		$Label.set_scroll_active(1)
		for line in max_lines:
			$Label.remove_line(0)
		scroll = true

func addLines(text):
	var text_width = 0
	text_width += font.get_string_size(' > ')[0]
	for letter in text:
		text_width += font.get_string_size(letter)[0]
	var additional_lines = floor(text_width / window_size[0])
    total_lines += 1 + additional_lines

	total_lines = total_lines * 2

The function addPlaceholderSpaces adds as many newlines as there are visible lines in the text label (found by dividing window height in pixels by the font height in pixels).

Then, the function to add text checks whether the current line count is larger than the max_lines variable divided by two (i. e. when the text fills one whole “page”) and if it is, the page full of newlines at the top is deleted and scrolling is enabled (and the global variable scroll is set to true so it won’t keep deleting the actual logs), making everything look good.

The addLines function is necessary to handle text strings that span more than the width of the label and are thus automatically split into multiple lines. The get_line\count() function doesn’t account for this, and only considers thsese single lines. The function checks whether the combined width of all the characters in the input string exceeds the width in pixels of the label window.

:bust_in_silhouette: Reply From: Ratty

Hi, landed here looking for solutions and (thanks to these suggestions) came up with another variation that might help.

Assuming the RichTextControl is a child of some container that matches it’s colour (so the RichTextControl does not show up when smaller than the parent) you can alter the margin_top to be the inverse of the content height, so it expands as content is added IE:

# get_content_height() is bugged - https://github.com/godotengine/godot/issues/36381
#margin_top = max(get_parent().rect_size.y - get_content_height(), 0)
margin_top = max(get_parent().rect_size.y - (get_line_count() * get_font("normal_font").height) - 32, 0)

The max() prevents it going smaller than zero (zero is full height).
The -32 above is to allow room for the LineEdit control below the RichTextControl.