Advice on Making a Virtual Keyboard that Works with a TextEdit Node

Godot Version

4.7

Question

So I’m in the midst of creating a virtual keyboard for my game which needs to be compatible with the mouse cursor, gamepads, and VR tracked controllers, and while I’ve made good progress I’ve hit a big stumbling block in that the built-in TextEdit control node does not show the caret while it does not have the focus; I’m kinda at a loss as to how to work around this.`

As such, I’m just asking for general advice on how to go about making a virtual keyboard which works around this quirk of the TextEdit control. If there’s a way to force the TextEdit control to always show the caret, even when it doesn’t have the focus, this would be a non-issue and I could continue making my virtual keyboard as intended, otherwise I am mostly curious how other Godot developers have tackled this.

And to answer the question as to why I’m not using Godot’s built-in virtual keyboard, there’s a few reasons. The main reason is that to avoid security holes with rich text, I’m not giving users access to rich text directly, instead parsing my own custom codes through non-rich text so that only my own scripting has access to rich text. So players can learn those custom codes there are extra buttons on the virtual keyboard for inserting them. The secondary reasons are so that I could make the virtual keyboard stylized to my own design and so that it works in VR appropriately using the UI-to-VR interface I already put in place.

It’s not possible to force drawing the caret when the TextEdit has no focus unless you enable TextEdit.caret_draw_when_editable_disabled but then you’ll need to disable TextEdit.editable which makes the TextEdit not editable directly. You can still edit it via code.

Another option is to draw the caret yourself with TextEdit.get_caret_draw_pos()

Example:

extends Node


@onready var text_edit: TextEdit = %TextEdit
@onready var keyboard_container: GridContainer = %KeyboardContainer

const KEYS = ["Q","W","E","R","T","Y","U","I","O","P", "A","S","D","F","G","H","J","K","L", "Z","X","C","V","B","N","M"]


func _ready() -> void:
	fill_keyboard.call_deferred()
	text_edit.draw.connect(func():
		var line_h = text_edit.get_line_height()
		var pos = text_edit.get_caret_draw_pos()
		text_edit.draw_line(Vector2(pos.x, pos.y - line_h), pos, Color.RED)
	)


func fill_keyboard() -> void:
	for key in KEYS:
		var b = Button.new()
		b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
		b.size_flags_vertical = Control.SIZE_EXPAND_FILL
		b.text = key
		b.pressed.connect(func():
			text_edit.insert_text_at_caret(key)
		)
		keyboard_container.add_child(b)


	var b = Button.new()
	b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	b.size_flags_vertical = Control.SIZE_EXPAND_FILL
	b.text = "ENTER"
	b.pressed.connect(func():
		text_edit.insert_text_at_caret("\n")
	)
	keyboard_container.add_child(b)

	b = Button.new()
	b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	b.size_flags_vertical = Control.SIZE_EXPAND_FILL
	b.text = "SPACE"
	b.pressed.connect(func():
		text_edit.insert_text_at_caret(" ")
	)
	keyboard_container.add_child(b)

Result:

I discovered the possibility of mimicking the text caret very late last night before bed and did some initial tests… That absolutely seems like the best approach, at least for what I have planned. The big trick is to ensure that real keystrokes are passed through to the TextEdit object appropriately when it doesn’t have focus, but I don’t think that issue’s going to be too challenging to solve; I’m imagining I can either pass along the keystrokes to the TextEdit object or maybe force it to gain the focus when a keystroke is made.

But yeah, definitely on the right track now! Still, if anyone has any other advice on this subject I would love to hear it! :B