Trying to develop an UI similar to this one, how should I do it?

Godot Version

Godot v4.2.1

Question

Hey there, I am trying to do an UI menu screen similar to the one that I am showing below. In summary, the UI would basically work like this. The red rectangle is an active selector that when it hovers on the buttons, the text inside with change the color. I don’t know if this was the best way to explain it that is pretty much it. I am curious on what the best approach is to develop something like this?

The UI Menu in Question

There are a few ways to do it. I chose to do it this way:

scene.tscn
[gd_scene load_steps=9 format=3 uid="uid://sjg5alv6x7oy"]

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_w41gh"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.1, 0.1, 0.1, 0.3)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ql218"]
content_margin_left = 16.0
content_margin_top = 8.0
content_margin_right = 16.0
content_margin_bottom = 8.0
bg_color = Color(0.1, 0.1, 0.1, 0.6)
draw_center = false
border_width_left = 4
border_width_top = 4
border_width_right = 4
border_width_bottom = 4
border_color = Color(0.0313726, 0.0313726, 0.0313726, 1)
corner_detail = 5

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_y8nx8"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.1, 0.1, 0.1, 0.6)
draw_center = false
border_width_left = 4
border_width_top = 4
border_width_right = 4
border_width_bottom = 4
border_color = Color(0.0313726, 0.0313726, 0.0313726, 1)
corner_detail = 5

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_byxf3"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.1, 0.1, 0.1, 0.6)
draw_center = false
border_width_left = 4
border_width_top = 4
border_width_right = 4
border_width_bottom = 4
border_color = Color(0.0313726, 0.0313726, 0.0313726, 1)
corner_detail = 5

[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_h44nm"]

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k5nuc"]
content_margin_left = 0.0
content_margin_top = 0.0
content_margin_right = 0.0
content_margin_bottom = 0.0
bg_color = Color(1, 1, 1, 1)
corner_detail = 5

[sub_resource type="Theme" id="Theme_bs2pt"]
Button/colors/font_color = Color(0, 0, 0, 1)
Button/colors/font_disabled_color = Color(0.875, 0.875, 0.875, 0.5)
Button/colors/font_focus_color = Color(1, 1, 1, 1)
Button/colors/font_hover_color = Color(0, 0, 0, 1)
Button/colors/font_hover_pressed_color = Color(1, 1, 1, 1)
Button/colors/font_outline_color = Color(1, 1, 1, 1)
Button/colors/font_pressed_color = Color(1, 1, 1, 1)
Button/colors/icon_disabled_color = Color(1, 1, 1, 0.4)
Button/colors/icon_focus_color = Color(1, 1, 1, 1)
Button/colors/icon_hover_color = Color(1, 1, 1, 1)
Button/colors/icon_hover_pressed_color = Color(1, 1, 1, 1)
Button/colors/icon_normal_color = Color(1, 1, 1, 1)
Button/colors/icon_pressed_color = Color(1, 1, 1, 1)
Button/constants/h_separation = 4
Button/constants/icon_max_width = 0
Button/constants/outline_size = 0
Button/font_sizes/font_size = 28
Button/styles/disabled = SubResource("StyleBoxFlat_w41gh")
Button/styles/focus = SubResource("StyleBoxFlat_ql218")
Button/styles/hover = SubResource("StyleBoxFlat_y8nx8")
Button/styles/normal = SubResource("StyleBoxFlat_ql218")
Button/styles/pressed = SubResource("StyleBoxFlat_byxf3")
Label/colors/font_color = Color(0, 0, 0, 1)
Label/colors/font_outline_color = Color(1, 1, 1, 1)
Label/colors/font_shadow_color = Color(0, 0, 0, 0)
Label/constants/line_spacing = 3
Label/constants/outline_size = 0
Label/constants/shadow_offset_x = 1
Label/constants/shadow_offset_y = 1
Label/constants/shadow_outline_size = 1
Label/font_sizes/font_size = 16
Label/styles/normal = SubResource("StyleBoxEmpty_h44nm")
PanelContainer/styles/panel = SubResource("StyleBoxFlat_k5nuc")

[sub_resource type="GDScript" id="GDScript_1h12d"]
script/source = "extends PanelContainer

@onready var selector: ColorRect = %Selector

func _enter_tree() -> void:
	# On enter tree we connect to SceneTree.node_added signal to find all the buttons that will be added
	# and connect to their signals
	get_tree().node_added.connect(func(node:Node):
		if node is Button:
			# grab focus when mouse entered so we can play the animation on mouse hover
			node.mouse_entered.connect(node.grab_focus)
			# on focus entered move the selector to the button
			node.focus_entered.connect(self._move_selector.bind(node))
			# on focus exited reset the colors of the button text
			node.focus_exited.connect(self._reset_button_font_color.bind(node))
	)


func _ready() -> void:
	# Wait a frame so the children are correctly positioned and focus the first valid node
	await get_tree().process_frame
	var control = find_next_valid_focus()
	if control:
		# grab the focus and move the selector to the node
		selector.global_position = control.global_position
		selector.size = control.size
		control.grab_focus.call_deferred()


func _change_font_colors(color:Color, button:Button) -> void:
	# Change the focus and hover colors of the button
	button.add_theme_color_override(\"font_focus_color\", color)
	button.add_theme_color_override(\"font_hover_color\", color)


func _move_selector(node:Button) -> void:
	# Create a tween and bind it to the selector
	var tween = get_tree().create_tween().bind_node(selector)
	# setup the ease and transition
	tween.set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_QUAD)
	# set it to be parallel, every tween will run at the same time
	tween.set_parallel(true)
	# tween the global position
	tween.tween_property(selector, \"global_position\", node.global_position, 0.12)
	# tween the size
	tween.tween_property(selector, \"size\", node.size, 0.09)
	# tween the font colors by calling a method
	tween.tween_method(_change_font_colors.bind(node), Color.BLACK, Color.WHITE, 0.12)
	selector.show()


func _reset_button_font_color(node:Button) -> void:
	# Create a tween and bind it to the selector
	var tween = get_tree().create_tween().bind_node(selector)
	# setup the ease and transition
	tween.set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_QUAD)
	# tween the font colors by calling a method
	tween.tween_method(_change_font_colors.bind(node), Color.WHITE, Color.BLACK, 0.12)
"

[node name="TestRectangleSelectorGUI" type="Node"]

[node name="PanelContainer" type="PanelContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = SubResource("Theme_bs2pt")
script = SubResource("GDScript_1h12d")

[node name="Control" type="Control" parent="PanelContainer"]
layout_mode = 2
mouse_filter = 2

[node name="Selector" type="ColorRect" parent="PanelContainer/Control"]
unique_name_in_owner = true
layout_mode = 2
offset_right = 40.0
offset_bottom = 40.0
mouse_filter = 2
color = Color(1, 0, 0, 1)

[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
layout_mode = 2
theme_override_constants/margin_left = 64
theme_override_constants/margin_top = 0
theme_override_constants/margin_right = 64
theme_override_constants/margin_bottom = 32

[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 16

[node name="Label" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
theme_override_font_sizes/font_size = 48
text = "HIGH
MID
LOW"
horizontal_alignment = 1

[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
size_flags_stretch_ratio = 2.0
theme_override_constants/separation = 24
alignment = 1

[node name="Button" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/VBoxContainer"]
custom_minimum_size = Vector2(280, 52)
layout_mode = 2
size_flags_horizontal = 4
text = "Arcade"

[node name="Button2" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/VBoxContainer"]
custom_minimum_size = Vector2(280, 52)
layout_mode = 2
size_flags_horizontal = 4
text = "Vs"

[node name="Button3" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/VBoxContainer"]
custom_minimum_size = Vector2(280, 52)
layout_mode = 2
size_flags_horizontal = 4
text = "Training"

[node name="Button4" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/VBoxContainer"]
custom_minimum_size = Vector2(280, 52)
layout_mode = 2
size_flags_horizontal = 4
focus_neighbor_left = NodePath("../../HBoxContainer2/Button")
focus_neighbor_right = NodePath("../../HBoxContainer2/Button3")
focus_neighbor_bottom = NodePath("../../HBoxContainer2/Button")
focus_next = NodePath("../../HBoxContainer2/Button")
text = "Options"

[node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 20
alignment = 1

[node name="Button" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer2"]
layout_mode = 2
text = "Horizontal"

[node name="Button2" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer2"]
layout_mode = 2
text = "Buttons"

[node name="Button3" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer2"]
layout_mode = 2
text = "With a long one just to test things"

[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
alignment = 1

[node name="Control" type="Control" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3

[node name="Label" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "H - Exit"
horizontal_alignment = 1
vertical_alignment = 1

[node name="Label2" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "K - Select"
horizontal_alignment = 1
vertical_alignment = 1

[node name="Control2" type="Control" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3

Result:

I ran out of time to explain it. Feel free to poke around the scene and check the Theme, layout, the script,… Good luck!

1 Like

WOW! This is incredible! Thank you so much. Is it ok if you could tell me a bit more about your approach? I would love to know.

Basically, the buttons use a StyleBoxFlat stylebox for pretty much all their states (normal, hover, pressed,…) where only the border is drawn, the background is disabled so we can show the selector behind them. We also change the different font colors states (normal, hover, pressed,…)

The red selector is just a ColorRect. When we find a target we tween its position and size to the target’s position and size. In my example, because the parent is a Container we need to “detach” it from the container by adding a Control as a parent of the ColorRect or it would make the ColorRect reposition and resize whenever the container requested sorting its children.

To find a target we connect to the SceneTree.node_added signal in Node._enter_tree() (the first time we have access to the SceneTree) to filter all the Buttons that are added to the scene and connect to their signals a couple of things:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.