Add more tools to conveniently work with hierarchies, especially in the presence of ghost nodes
What problem does this solve or what need does it fill?
Following the introduction of https://github.com/bevyengine/bevy/pull/15341, some users have expressed concerns around difficulties migrating to account for ghost nodes in their hierarchies.
This is a great opportunity to add more tools for traversing hierarchies in the spirit of iter_descendants.
What solution would you like?
There are two stages to this:
- Add more methods to
HierarchyQueryExtto capture other common tasks. - Add a ghost-node aware version of this trait, which lives in
bevy_uiand takes aHas<GhostNode>argument as well. These methods should be namediter_node_descendantsand so on.
In theory these can be done in either order, or in a single PR. Your choice!
In terms of additional tasks to cover:
root_parent: the entity that is highest up the hierarchy tree, if any.leaf_children: an iterator over the children that have no children of their own.siblings: other entities that are in direct children of the same parent.parent: a convenience method that looks for the parent (much more useful when working with ghost nodes).children: as above, but for children.
What alternative(s) have you considered?
One day (🥺), this will get rolled into #3742. It's nice to ship helpful APIs to users sooner though, and these methods will be easier to migrate since they have semantic meaning.
The UiChildren SystemParam tackles a good chunk of task 2. I'm going to start on task 1, get that merged, and then beef up that API to match it.
With the first half of this issue is merged (#15627), we can focus on the second half: UI tree.
The second half
For the second half of this issue, we would want to make UIChildren have feature parity with HierarchyQueryExt.
I propose we also change name of UIChildren to UiTree to better convey what it is. This could also serve as a good place to document the rules of the tree (e.g. has to be all Node/GhostNode up to the root, non-UI entities beneath the tree are allowed/ignored)
I looked over the APIs and summarized the current state along with proposed UiTree APIs:
HierarchyQueryExt (current) |
UiChildren (current) |
UiTree (proposal) |
Notes |
|---|---|---|---|
parent |
get_parent |
parent |
|
children |
iter_ui_children |
children |
Alternative name iter_children to indicate it is an iterator rather than a slice. |
root_ancestor |
missing | root_ancestor |
|
iter_leaves |
missing | iter_leaves |
|
iter_siblings |
missing | iter_siblings |
|
iter_descendants |
missing | iter_descendants |
|
iter_descendants_depth_first |
missing | iter_descendants_depth_first |
|
iter_ancestors |
missing | iter_ancestors |
|
| missing | is_changed |
children_is_changed |
Could be added to hierarchy with #15670 |
| missing | missing | parent_is_changed |
Could be added to hierarchy with #15670 |
| irrelevant | iter_ghost_nodes |
iter_ghost_nodes |
Bonus utilities
In addition to the above, we could also add more utilities that are useful in UI, here is a list inspired by CSS psuedo-classes:
is_root |
No parent or only ghost node ancestors |
is_empty |
No children |
child_index |
The index of a child among its siblings |
is_nth_child |
child_index(entity) == n |
is_nth_last_child |
child_index(entity) == children().len() - n |
is_first_child |
child_index(entity) == 0 |
is_last_child |
child_index(entity) == children().len() - 1 |
is_odd_child |
(child_index(entity) + 1) % 2 == 1 |
is_even_child |
(child_index(entity) + 1) % 2 == 0 |
Maybe something for a follow-up though, and I'm not sure how much bloat we want in the API.
Excellent investigation. Of the bonus APIs, I like is_root, is_empty (renamed to is_leaf) and child_index. The rest don't feel useful to me.
I believe that @ickshonpe has previously expressed opposition to this approach. If I remember correctly, they believe that the proposed wrapper abstractions don't actually help all that much, and have noticeable costs. A straightforward recursive traversal of the hierarchy is faster than a fancy iterator, and may be easier to maintain in the long run.
Some more detail on this:
As the person who (at this time) is the primary user of ghost nodes, I want to share my impressions. None of my uses of ghost nodes involves any kind of traversal. I never use the UiChildren iterator. The reason is because, as a client, the way I used ghost nodes is purely local and context-free: I insert ghost nodes wherever I need dynamic control flow, but I don't need to know anything about the parent or children in order to do this. I just assume they work.
The places where traversal needs to know about ghost nodes are all inside Bevy itself: computing visibility, transforms, layout, picking, and all the other things that the framework does. As a user, I don't invoke any of this code directly and I don't care if the APIs are convenient or not.
(This is not to say that I never do traversal as a user. But the places where I do traversal don't care about ghosts.)
So really, the issue is not "how do we make hierarchy traversal better for users", because I don't think we need to do this. The real issue is how we get the engine code to do traversal consistently with little or no performance impact.
@viridia
I think this issue was mainly motivated by third party crate author concerns with ghost nodes and having to take them into account for their UI traversal. I would guess this is more apparent in non-reactive UI utilities that need to look up and down the hierarchy, and may not even be relevant in the future.
However, the initial motivation behind UiChildren / UiRootNodes was not primarily to make things easier for end users. It was about ensuring correctness within bevy_ui itself. There are (or at least were, maybe this has changed) a lot of places that query for UI roots and traverse the tree. Adding special logic for ghost nodes in each of these places seemed bug-prone and verbose.
If they have serious impacts on performance that can't be fixed I agree to remove them though. There are optimizations that can be made within those iterators though, which I did not do in the initial ghost node PR (for example iter_ui_children allocates on every level with > 8 children).