From 5b29402cc8da0ad49ed6a235a0c09bd8fb6c1642 Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Fri, 2 Aug 2024 08:37:15 -0700 Subject: [PATCH] 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` 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. --- crates/bevy_hierarchy/src/child_builder.rs | 61 ++++++++++++++++++++++ examples/ui/button.rs | 18 +++---- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/crates/bevy_hierarchy/src/child_builder.rs b/crates/bevy_hierarchy/src/child_builder.rs index ba772d328d..95298f0d71 100644 --- a/crates/bevy_hierarchy/src/child_builder.rs +++ b/crates/bevy_hierarchy/src/child_builder.rs @@ -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(&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(&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(&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_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::(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::(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::>(); @@ -859,6 +888,19 @@ mod tests { assert_eq!(*world.get::(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::(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::::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); + } } diff --git a/examples/ui/button.rs b/examples/ui/button.rs index edcb4909cd..0a984f7a50 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -83,15 +83,13 @@ fn setup(mut commands: Commands, asset_server: Res) { background_color: NORMAL_BUTTON.into(), ..default() }) - .with_children(|parent| { - parent.spawn(TextBundle::from_section( - "Button", - TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 40.0, - color: Color::srgb(0.9, 0.9, 0.9), - }, - )); - }); + .with_child(TextBundle::from_section( + "Button", + TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 40.0, + color: Color::srgb(0.9, 0.9, 0.9), + }, + )); }); }