godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.05k stars 65 forks source link

Add a control suitable for displaying long lists with customizable content #9678

Open timothyqiu opened 1 week ago

timothyqiu commented 1 week ago

Describe the project you are working on

A log console inside a game. And tweaking the Godot editor.

Describe the problem or limitation you are having in your project

In order to display a long list, there are two options:

  1. ItemList or Tree
    • Optimized for long lists, but lack of customization. The layout for each row is fixed.
  2. BoxContainer + ScrollContainer
    • Fully customizable, but poor performance for long lists.

Option 2 is the only way to go if I want to customize row content. But, for example, I have to literally instantiate 10000 rows of controls for the list.

Option 1 also has something that need to be addressed. It's almost a convention in editor code that Trees are cleared and rebuilt once something changes. From the perspective of the API, it's inconvenient to update only a row when something changes. Tree users have to keep track of the mapping between data element and TreeItem.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Implement something like the Android RecyclerView and iOS UICollectionView (or UITableView which is a simplified version).

The basic idea is that the user provides an Adapter (data source) for the list control.

The list control reuses the view created previously when possible. Only a few views will be created for a long list.

How the list control arranges views is determined by a similar Layout object (e.g. vertically, horizontally, coverflow, paged, waterfall...).

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

See the reference links above for details.

Demo usage:

@onready var list: RecyclerView = $List

func _ready():
    var adapter = UserAdapter.new()
    adapter.users = get_users()
    list.adapter = adapter

class UserAdapter:
    var users: Array[User]

    func _get_item_count() -> int:
        return users.size()

    # Called by RecyclerView when a new view should be created.
    # `type` is 0 by default. Override `_get_view_type(position: int) -> int` to use different views.
    func _create_view(type: int) -> Control:
        return preload("res://user_view.tscn").instantiate()

    # Called by RecyclerView when a new row will be shown.
    # `view` is a reused control, or newly created by `_create_view()`.
    func _bind_view(view: Control, position: int) -> void:
        var user_view := view as UserView
        var user_data := users[position]
        user_view.username.text = user_data.name
        user_view.age.text = str(user_data.age)
        user_view.load_avatar(user_data.avatar_url)
        user_view.removed_pressed.connect(func ():
            users.remove_at(position)  # Update data model.
            list.reload_data()  # Refresh the list, or
            list.delete_item(position)  # Only update row count & rows from position. Animation possible.
        )

    # Called by RecylerView when a view is just hidden. You can do some cleanup here.
    func _recycle_view(view: Control) -> void:
        var user_view := view as UserView
        user_view.cancel_avatar_loading()
        user_view.disconnect_signals()

If this enhancement will not be used often, can it be worked around with a few lines of script?

Not a few lines.

Is there a reason why this should be core and not an add-on in the asset library?

It fills the gap between ItemList/Tree and BoxContainer+ScrollContainer.