|
| 1 | +# Control List for Godot |
| 2 | + |
| 3 | +A small Godot addon that renders a dynamic list of items as UI Controls. |
| 4 | + |
| 5 | +- Drop a ControlList node under any Control container. |
| 6 | +- Give it an array of items. |
| 7 | +- It will insert, delete, and move child Controls to match your data efficiently |
| 8 | + using [the Myers diffing algorithm](https://www.nathaniel.ai/myers-diff/). |
| 9 | +- Existing Controls are reused when possible, so your per-item state stays intact. |
| 10 | + |
| 11 | +Useful for inventories, chat/message feeds, settings lists, leaderboards, etc. |
| 12 | + |
| 13 | +This is similar to Qt's ListView and Android's RecyclerView+DiffUtil. |
| 14 | + |
| 15 | +## Features |
| 16 | + |
| 17 | +- Reuses existing nodes when items are the “same” (you decide what “same” means) |
| 18 | +- Can be placed in any container that handles multiple items (HFlowContainer, VBoxContainer…) |
| 19 | +- Can exist alongside other Controls in the same container, such as headers and footers |
| 20 | +- Simple factory API - return any Control you like (Label, HBoxContainer, custom scene…) |
| 21 | +- Deferred updates by default to prevent redundant updates, with an option to update immediately |
| 22 | + |
| 23 | +## Install |
| 24 | + |
| 25 | +There are two options: |
| 26 | + |
| 27 | +1. From the Asset Library |
| 28 | + |
| 29 | +2. From a release |
| 30 | + - Download the control-list.zip from the GitHub Releases of this repo. |
| 31 | + - Unzip into your project so it lands at: addons/control-list |
| 32 | + |
| 33 | +## Quick start |
| 34 | + |
| 35 | +1. Add a ControlList node as a child under any Control container that should hold your items (e.g. a VBoxContainer). |
| 36 | +2. Provide a factory function to build a Control for each item: `set_control_factory` |
| 37 | +3. (Recommended) Provide a key function that returns a stable identity for each item: `set_item_key` |
| 38 | +4. Call `update_list(new_items)` whenever your data changes. |
| 39 | + |
| 40 | +Example: |
| 41 | + |
| 42 | +<img src="./misc/screenshot_simplescenetree.png"> |
| 43 | + |
| 44 | +```gdscript |
| 45 | +@onready var cl: ControlList = $VBoxContainer/ControlList |
| 46 | +
|
| 47 | +func _ready(): |
| 48 | + # Create a simple Label for each item |
| 49 | + cl.set_control_factory( |
| 50 | + func(item): |
| 51 | + var label := Label.new() |
| 52 | + label.text = item.name |
| 53 | + return label |
| 54 | + ) |
| 55 | + |
| 56 | + # Use a stable key so items are reused/moved |
| 57 | + cl.set_item_key(func(item): return item.id) |
| 58 | + |
| 59 | + # Render items |
| 60 | + cl.update_list([ |
| 61 | + { "id": "item_sword", "name": "Sword" }, |
| 62 | + { "id": "item_shield", "name": "Shield" }, |
| 63 | + { "id": "item_helmet", "name": "Helmet" }, |
| 64 | + ]) |
| 65 | +
|
| 66 | +
|
| 67 | +# Later… update with new content |
| 68 | +cl.update_list([ |
| 69 | + { "id": "item_shield", "name": "Shield" }, # moved |
| 70 | + { "id": "item_bow", "name": "Bow" }, # inserted |
| 71 | + { "id": "item_helmet", "name": "Helmet" }, # moved |
| 72 | +]) |
| 73 | +``` |
| 74 | + |
| 75 | +## Sample project |
| 76 | + |
| 77 | +See the sample/ directory for a small example of a shop UI. |
| 78 | +ControlList is set up in [sample_shop.gd](./sample/sample_shop.gd). |
| 79 | + |
| 80 | +## How it works |
| 81 | + |
| 82 | +ControlList looks at the previous item array vs the new one. |
| 83 | +It runs a Myers diff and computes insert/delete/move operations. |
| 84 | +It applies those operations to child Controls under the ControlList’s parent. |
| 85 | +If keys match, nodes are reused and only moved. If a key disappears, the node is freed. |
| 86 | + |
| 87 | +## Important notes |
| 88 | + |
| 89 | +- Parent must be a Control. Add ControlList as a child under the container that should hold your list items. |
| 90 | +- Items are added as siblings next to the ControlList (children of its parent), |
| 91 | + ordered immediately after the ControlList. |
| 92 | +- Your factory should create fresh Controls for inserts. Visual updates for existing items are up to you. |
| 93 | + |
| 94 | +## Testing |
| 95 | + |
| 96 | +This repo includes GdUnit4 tests. |
| 97 | + |
| 98 | +- You’ll need Godot 4.4 installed. Tests currently fail on 4.5, |
| 99 | + but it seems to be an issue with the test runner, not this library. |
| 100 | +- Set the environment variable GODOT_BINARY to your Godot executable. |
| 101 | +- Run: `./run_all_tests.sh` |
| 102 | + |
| 103 | +Example on macOS: |
| 104 | + |
| 105 | +```bash |
| 106 | +export GODOT_BINARY=/Applications/Godot.app/Contents/MacOS/Godot |
| 107 | +./run_all_tests.sh |
| 108 | +``` |
| 109 | + |
| 110 | +## Compatibility |
| 111 | + |
| 112 | +- Tested on Godot 4.5 and 4.4. |
| 113 | + |
| 114 | +## Future plans |
| 115 | + |
| 116 | +- Maybe: Content change notifications |
| 117 | + - Given a function that determines whether two items have equal data, |
| 118 | + notify that that item should be updated. |
| 119 | + I'm used to this pattern from mobile UI development, but not sure |
| 120 | + if it makes as much sense in Godot. |
| 121 | +- Maybe: Make position known to items |
| 122 | +- Maybe: Background thread diffing |
0 commit comments