Search code examples
godotgdscript

Is there a `HVBoxContainer` for Godot?


Think of an RPG game where you might need to present a list of buttons. A user might enter a room where they have to select from a series of options (buttons). Is there a type of container/panel that would show clickable buttons horizontally, but wrap if needed?

The best analogy I can think of to picture the situation is, Imagine needing to click on an item in a backpack, but each item is potentially a different width. (I can make them all the same height but the width then varies)

.---[My Backpack]------.
| aaa  bbb  cccc ddd   |
| ee  fff  g           |
|                      |
|                      |
`----------------------'

(The options come from a database, so its unknown at compile time how many options might be in a room, so I am going to need to programmatically add options.)

The very bottom of this godot document introduces custom container layouts, but it's unclear to me how this would work in practice


Solution

  • Flow container for Godot 3.5 or newer

    Godot 3.5 (currently in beta) introduces HFlowContainer and VFlowContainer that will serve the propuse described.

    The HFlowContainer will fill a row and when they overflow, it will add a new row and continue there. The VFlowContainer will work on a similar fashion but with columns.


    Flow containers before Godot 3.5

    For older versions of Godot you can use the HFlowContainer addon which you can find it in the asset library (here). Note that there is no VFlowContainer counterpart.

    As everything on the asset library it is free and open source, so feel free to read the code and modify it, which can be serve as starting point if you want to make your own custom Container.


    Making your own custom Container

    The gist of making a custom Container is that it must position its children.

    For that effect you react to NOTIFICATION_SORT_CHILDREN in the _notification method. You might also want to react to NOTIFICATION_RESIZED.

    You can have a method - which I'll call layout - that you call when you get the notifications:

    func _notification(what):
        if what == NOTIFICATION_SORT_CHILDREN:
            layout()
    

    And also call layout from the setters (setget) of the properties that define how the Container must organize its children. To call layout from anywhere other than _notification, you might want to use call_deferred("layout") to prevent any possible re-layout loops from hanging or crashing the game.


    The layout method would iterate over the visible children Controls and use get_combined_minimum_size to figure out their size.

    Something like this:

    func layout() -> void:
        # …
        for child in get_children():
            var control := child as Control
            if not is_instance_valid(control) or not control.visible:
                continue
    
            var size := control.get_combined_minimum_size()
        # …
    

    Then using that information compute the position and size for the children Controls. When there is room for the Controls to grow, you may want to split it among them according to their size_flags_stretch_ratio.

    Once you have the position and size for a Control decided, use fit_child_in_rect to position them, which will take into account grow and size flags.

    Thus - barring the simplest Containers - you will need to iterate over the children Controls twice. And for that you might find useful to have an auxiliary data structure to temporarily store them.