How to make folding menu?

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

How to make a folding menu like this:
(they looks like a drawer, usually its closes.
When one is opened, expand the window it contains and squeeze down the other drawers)

1 Like
:bust_in_silhouette: Reply From: flurick

Here is an example of a custom node that works like an accordion.

enter image description here

enter image description here

The Control moves each child to the end of the previous one (only in the y direction)
enter image description here

extends Control

export var spacing = 10

func _draw():
	
	var last_end_achor = Vector2.ZERO
	for child in get_children():
		child.rect_position = last_end_achor
		last_end_achor.y = child.rect_position.y + child.rect_size.y 
		last_end_achor.y += spacing
	
	rect_min_size.y = last_end_achor.y #to work with ScrollContainer

And clicking on the button “show” toggles the parent “Panel” size to the open or the closed value.
enter image description here

extends Panel

var is_expanded = false


func _ready():
	$VBoxContainer/show.connect("pressed",self,"expand")


func expand():
	is_expanded = !is_expanded


var last_rect_size = Vector2.ZERO
func _process(delta):
	
	#snap to end
	if abs(rect_size.y-rect_min_size.y) < 1:
		rect_size.y = rect_min_size.y
	
	#resize to target size
	if is_expanded:
		rect_size.y = lerp(rect_size.y, 70, 0.1)
	else:
		rect_size.y = lerp(rect_size.y, rect_min_size.y, 0.1)
	
	#update layout
	if last_rect_size != rect_size:
		get_parent().update()
		last_rect_size = rect_size

Wow!!!Thanks!!

fish2091852127 | 2019-03-18 08:05

I followed your instruction to make this menu. But the items in panel didn’t close. Can you help?

hoody monkey | 2019-12-06 02:29

What I have done to make this work is to make the show button a child of the root and then set “Clip Content” in the panel rect properties to true.

Now the content of the panel will shrink with the panel.

If I find the time I will post an github project with an working reusable example.

R. K. | 2020-02-11 10:22

For me that solution doesn’t work well with spinbox. It makes an overflow due to callback update. I have done a simpler script on a VBoxContainer and a button “show” as his child and it seems to work.

extends VBoxContainer

export var is_expanded = true

func _ready():
	$show.connect("pressed",self,"expand")
	get_parent().connect("expand",self,"resized")


func expand():
	is_expanded = !is_expanded
	for child in get_children():
		child.visible = true if is_expanded else child == $show

m21-cerutti | 2022-02-21 20:18

2 Likes

These answers not work in godot 4. Just add a solution of mine.
This script is linked to the ExpandableVbox, and the vbox is saved to a branch scene. So that it can be used in other scene(just drag nodes as children)
微信截图_20240927144258
You can call theexpand method of it by other signals like button, or just enable the _gui_input function(and replace 0 with min size)

extends VBoxContainer

@export var is_expanded = true

enum STATE {OPEN,CLOSE,OPENING,CLOSING}

var init = false
var state :STATE
var max_size : Vector2i
var last_size : Vector2i

@onready var v_box = $"."

func _ready():
    state = STATE.OPEN


#func _gui_input(event):
    #if event is InputEventMouseButton:
        #if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
            #self.expand()

func expand():
    is_expanded = !is_expanded
    if is_expanded:
        state = STATE.OPENING
    else:
        state = STATE.CLOSING


func _process(delta):
    if not init: # not in ready beacuse ready do not get corret size
        max_size = v_box.size
        last_size = v_box.size
        v_box.custom_minimum_size.y = max_size.y
        init = true
                
    if state == STATE.CLOSING:
        v_box.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
        #print(last_size)
        if v_box.custom_minimum_size.y > 0:
            v_box.custom_minimum_size.y = lerp(last_size.y,0,0.1)
            last_size = v_box.custom_minimum_size
        elif v_box.custom_minimum_size.y == 0:
            v_box.size_flags_vertical = Control.SIZE_FILL
            for child in v_box.get_children():
                child.visible = true if is_expanded else child == $show
            state = STATE.CLOSE
    
    elif state == STATE.OPENING:
        v_box.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
        for child in v_box.get_children():
            child.visible = true if is_expanded else child == $show
        if v_box.custom_minimum_size.y < max_size.y:
            v_box.custom_minimum_size.y = lerp(last_size.y,max_size.y,0.1)
            last_size = v_box.custom_minimum_size
        elif v_box.custom_minimum_size.y == max_size.y:
            v_box.size_flags_vertical = Control.SIZE_FILL
            state = STATE.OPEN
    

Thank you for this! What does “child == $show” refer to here?

there is an easier way to do this. also works with scroll containers. with godot 4 (havent tested in in godot 3).

use a control node and inside it put your container nodes with their inputs/button/labels etc.
eg.
-control
—-collapse button/checkbutton/btn_container
—-container/scroll container(top level)
--------inputs/other containers with inputs

Set the custom_minimum_size.y of your top level container for inputs, and the control node, to whatever size for the content. + btn.custom_minimum_size.y for the control node.

set the button or button container custom_minimum_size.y to however big it has to be to show the button.

add signal from your button to your control node script.

add script to your control node in the signal function. eg.

func _on_check_button_toggled(toggled_on):
------if not toggled_on:
------------control_node.custom_minimum_size.y = collapse_button/button_container.custom_minimum_size.y
------------top_inputs_container.scale.y = 0
------else:
------------top_inputs_container.scale.y = 1
------------control_node.custom_minimum_size.y = top_inputs_container.size.y

remmeber to enable clip_contents on scroll_containers so the children dont show up outside the container.

you can instance/add the control nodes in another scroll container/other container, if you want, and they will still collapse.

explanation:
the control node size gives the volume to the content so nothing overlaps if put inside another container.

while the top level inputs container size shows the content (you can use show(), hide() instead of scaling it, if you want).