Add with_child to simplify spawning when there will only be one child (#14594)

# Objective

This idea came up in the context of a hypothetical "text sections as
entities" where text sections are children of a text bundle.

```rust
commands
    .spawn(TextBundle::default())
    .with_children(|parent} {
        parent.spawn(TextSection::from("Hello"));
    });
```

This is a bit cumbersome (but powerful and probably the way things are
headed). [`bsn!`](https://github.com/bevyengine/bevy/discussions/14437)
will eventually make this nicer, but in the mean time, this might
improve ergonomics for the common case where there is only one
`TextSection`.

## Solution

Add a `with_child` method to the `BuildChildren` trait that spawns a
single bundle and adds it as a child to the entity.

```rust
commands
    .spawn(TextBundle::default())
    .with_child(TextSection::from("Hello"));
```

## Testing

I added some tests, and modified the `button` example to use the new
method.

If any potential co-authors want to improve the tests, that would be
great.

## Alternatives

- Some sort of macro. See
https://github.com/tigregalis/bevy_spans_ent/blob/main/examples/macro.rs#L20.
I don't love this, personally, and it would probably be obsoleted by
`bsn!`.
- Wait for `bsn!`
- Add `with_children_batch` that takes an `Into<Iterator>` of bundles.
  ```rust
  with_children_batch(vec![TextSection::from("Hello")])
  ```
This is maybe not as useful as it sounds -- it only works with
homogeneous bundles, so no marker components or styles.
- If this doesn't seem valuable, doing nothing is cool with me.
This commit is contained in:
Rob Parrett 2024-08-02 08:37:15 -07:00 committed by GitHub
parent 4c2cef2223
commit 5b29402cc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 69 additions and 10 deletions

View File

@ -341,6 +341,9 @@ pub trait BuildChildren {
/// Takes a closure which builds children for this entity using [`ChildBuild`].
fn with_children(&mut self, f: impl FnOnce(&mut Self::Builder<'_>)) -> &mut Self;
/// Spawns the passed bundle and adds it to this entity as a child.
fn with_child<B: Bundle>(&mut self, bundle: B) -> &mut Self;
/// Pushes children to the back of the builder's children. For any entities that are
/// already a child of this one, this method does nothing.
///
@ -432,6 +435,13 @@ impl BuildChildren for EntityCommands<'_> {
self
}
fn with_child<B: Bundle>(&mut self, bundle: B) -> &mut Self {
let parent = self.id();
let child = self.commands().spawn(bundle).id();
self.commands().add(PushChild { parent, child });
self
}
fn push_children(&mut self, children: &[Entity]) -> &mut Self {
let parent = self.id();
if children.contains(&parent) {
@ -566,6 +576,17 @@ impl BuildChildren for EntityWorldMut<'_> {
self
}
fn with_child<B: Bundle>(&mut self, bundle: B) -> &mut Self {
let child = self.world_scope(|world| world.spawn(bundle).id());
if let Some(mut children_component) = self.get_mut::<Children>() {
children_component.0.retain(|value| child != *value);
children_component.0.push(child);
} else {
self.insert(Children::from_entities(&[child]));
}
self
}
fn add_child(&mut self, child: Entity) -> &mut Self {
let parent = self.id();
if child == parent {
@ -692,6 +713,14 @@ mod tests {
assert_eq!(world.get::<Children>(parent).map(|c| &**c), children);
}
/// Assert the number of children in the parent's [`Children`] component if it exists.
fn assert_num_children(world: &World, parent: Entity, num_children: usize) {
assert_eq!(
world.get::<Children>(parent).map(|c| c.len()).unwrap_or(0),
num_children
);
}
/// Used to omit a number of events that are not relevant to a particular test.
fn omit_events(world: &mut World, number: usize) {
let mut events_resource = world.resource_mut::<Events<HierarchyEvent>>();
@ -859,6 +888,19 @@ mod tests {
assert_eq!(*world.get::<Parent>(children[1]).unwrap(), Parent(parent));
}
#[test]
fn build_child() {
let mut world = World::default();
let mut queue = CommandQueue::default();
let mut commands = Commands::new(&mut queue, &world);
let parent = commands.spawn(C(1)).id();
commands.entity(parent).with_child(C(2));
queue.apply(&mut world);
assert_eq!(world.get::<Children>(parent).unwrap().0.len(), 1);
}
#[test]
fn push_and_insert_and_remove_children_commands() {
let mut world = World::default();
@ -1228,4 +1270,23 @@ mod tests {
let children = query.get(&world, parent);
assert!(children.is_err());
}
#[test]
fn with_child() {
let world = &mut World::new();
world.insert_resource(Events::<HierarchyEvent>::default());
let a = world.spawn_empty().id();
let b = ();
let c = ();
let d = ();
world.entity_mut(a).with_child(b);
assert_num_children(world, a, 1);
world.entity_mut(a).with_child(c).with_child(d);
assert_num_children(world, a, 3);
}
}

View File

@ -83,8 +83,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
background_color: NORMAL_BUTTON.into(),
..default()
})
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
.with_child(TextBundle::from_section(
"Button",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
@ -93,5 +92,4 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
));
});
});
}