use super::{
graph::{SignalHandle, SignalHandles},
signal::{Signal, SignalExt},
signal_map::{SignalMap, SignalMapExt},
signal_vec::{SignalVec, SignalVecExt, VecDiff},
utils::LazyEntity,
};
use bevy_ecs::{
component::Mutable,
lifecycle::HookContext,
prelude::*,
system::{IntoObserverSystem, RunSystemOnce},
world::{DeferredWorld, error::EntityMutableFetchError},
};
use bevy_platform::prelude::*;
use core::sync::atomic::{AtomicUsize, Ordering};
#[derive(Default)]
pub struct Builder {
#[allow(clippy::type_complexity)]
on_spawns: Vec<Box<dyn FnOnce(&mut World, Entity) + Send + Sync>>,
next_block: AtomicUsize,
}
impl Clone for Builder {
#[track_caller]
fn clone(&self) -> Self {
let msg = format!(
"Cloning `jonmo::Builder` at {} is a bug! Use a factory function instead.",
core::panic::Location::caller()
);
if cfg!(debug_assertions) {
cfg_if::cfg_if! {
if #[cfg(feature = "std")] {
let backtrace = std::backtrace::Backtrace::force_capture();
panic!("{}\nBacktrace:\n{}", msg, backtrace);
} else {
panic!("{}", msg);
}
}
}
#[cfg(feature = "tracing")]
bevy_log::error!("{}", msg);
Self::default()
}
}
impl<T: Bundle> From<T> for Builder {
fn from(bundle: T) -> Self {
Self::new().insert(bundle)
}
}
#[derive(Component, Default)]
struct ChildBlockPopulations(Vec<usize>);
impl ChildBlockPopulations {
fn ensure_block(&mut self, block: usize) {
if self.0.len() <= block {
self.0.resize(block + 1, 0);
}
}
}
impl Builder {
#[allow(missing_docs)]
pub fn new() -> Self {
Self::default()
}
pub fn on_spawn(mut self, on_spawn: impl FnOnce(&mut World, Entity) + Send + Sync + 'static) -> Self {
self.on_spawns.push(Box::new(on_spawn));
self
}
pub fn on_spawn_with_system<T, M>(self, system: T) -> Self
where
T: IntoSystem<In<Entity>, (), M> + Send + Sync + 'static,
{
self.on_spawn(|world, entity| {
#[allow(unused_variables)]
if let Err(error) = world.run_system_once_with(system, entity) {
#[cfg(feature = "tracing")]
bevy_log::error!("failed to run system on spawn: {}", error);
}
})
}
pub fn hold_tasks(self, tasks: impl IntoIterator<Item = Box<dyn SignalTask>> + Send + Sync + 'static) -> Self {
self.on_spawn(move |world, entity| {
let handles: Vec<_> = tasks.into_iter().map(|task| task.register_task(world)).collect();
add_handles(world, entity, handles);
})
}
pub fn with_entity(self, f: impl FnOnce(EntityWorldMut) + Send + Sync + 'static) -> Self {
self.on_spawn(move |world, entity| {
f(world.entity_mut(entity));
})
}
pub fn insert<T: Bundle>(self, bundle: T) -> Self {
self.with_entity(move |mut entity| {
entity.insert(bundle);
})
}
pub fn with_component<C: Component<Mutability = Mutable>>(
self,
f: impl FnOnce(Mut<C>) + Send + Sync + 'static,
) -> Self {
self.with_entity(|mut entity| {
if let Some(component) = entity.get_mut::<C>() {
f(component);
}
})
}
pub fn observe<E: EntityEvent, B: Bundle, Marker>(
self,
observer: impl IntoObserverSystem<E, B, Marker> + Sync,
) -> Self {
self.on_spawn(|world, entity| {
world.entity_mut(entity).observe(observer);
})
}
pub fn on_despawn(self, on_despawn: impl FnOnce(&mut DeferredWorld, Entity) + Send + Sync + 'static) -> Self {
self.on_spawn(|world, entity| {
let mut entity_mut = world.entity_mut(entity);
if let Some(mut on_despawn_component) = entity_mut.get_mut::<OnDespawnCallbacks>() {
on_despawn_component.0.push(Box::new(on_despawn));
} else {
entity_mut.insert(OnDespawnCallbacks(vec![Box::new(on_despawn)]));
}
})
}
#[track_caller]
pub fn lazy_entity(self, entity: LazyEntity) -> Self {
self.on_spawn(move |_, e| entity.set(e))
}
pub fn on_signal<I, S, F, M>(self, signal: S, system: F) -> Self
where
I: Clone + Send + Sync + 'static,
S: Signal<Item = I> + Send + Sync + 'static,
F: IntoSystem<In<(Entity, I)>, (), M> + Send + Sync + 'static,
{
let on_spawn = move |world: &mut World, entity: Entity| {
let handle =
Signal::register_signal(signal.map(move |In(input): In<I>| (entity, input)).map(system), world);
add_handles(world, entity, [handle]);
};
self.on_spawn(on_spawn)
}
pub fn on_signal_with_entity<I, S, F>(self, signal: S, mut f: F) -> Self
where
I: Clone + Send + Sync + 'static,
S: Signal<Item = I> + Send + Sync + 'static,
F: FnMut(EntityWorldMut, I) + Send + Sync + 'static,
{
self.on_signal(signal, move |In((entity, value)), world: &mut World| {
f(world.entity_mut(entity), value)
})
}
pub fn on_signal_with_component<I, C>(
self,
signal: impl Signal<Item = I>,
mut f: impl FnMut(Mut<C>, I) + Send + Sync + 'static,
) -> Self
where
C: Component<Mutability = Mutable>,
I: Clone + 'static,
{
let on_spawn = move |world: &mut World, entity: Entity| {
let handle = Signal::register_signal(
signal.map(move |In(input): In<I>, mut components: Query<&mut C>| {
if let Ok(component) = components.get_mut(entity) {
f(component, input)
}
}),
world,
);
add_handles(world, entity, [handle]);
};
self.on_spawn(on_spawn)
}
pub fn component_signal<C>(self, signal: impl Signal<Item = Option<C>>) -> Self
where
C: Component + Clone,
{
self.on_signal(
signal,
move |In((entity, component_option)): In<(Entity, Option<C>)>, world: &mut World| {
let mut entity = world.entity_mut(entity);
if let Some(component) = component_option {
entity.insert(component);
} else {
entity.remove::<C>();
}
},
)
}
pub fn child(self, child: impl Into<Builder>) -> Self {
let block = self.next_block.fetch_add(1, Ordering::Relaxed);
let child = child.into();
let on_spawn = move |world: &mut World, parent| {
let child_entity = world.spawn_empty().id();
let mut pops = world.get_mut::<ChildBlockPopulations>(parent).unwrap();
pops.ensure_block(block);
pops.0[block] = 1;
let insert_offset = offset(block, &pops.0);
EntityWorldMut::insert_children(&mut world.entity_mut(parent), insert_offset, &[child_entity]);
child.spawn_on_entity(world, child_entity).unwrap();
};
self.on_spawn(on_spawn)
}
pub fn child_signal(self, child_option: impl Signal<Item = Option<Builder>>) -> Self {
let block = self.next_block.fetch_add(1, Ordering::Relaxed);
let on_spawn = move |world: &mut World, parent: Entity| {
let mut pops = world.get_mut::<ChildBlockPopulations>(parent).unwrap();
pops.ensure_block(block);
let system = move |In(child_option): In<Option<Builder>>,
world: &mut World,
mut existing_child_option: Local<Option<Entity>>| {
if let Some(child) = child_option {
if let Some(existing_child) = existing_child_option.take() {
world.entity_mut(existing_child).despawn();
}
let child_entity = world.spawn_empty().id();
let pops = world.get::<ChildBlockPopulations>(parent).unwrap();
let insert_offset = offset(block, &pops.0);
world.entity_mut(parent).insert_children(insert_offset, &[child_entity]);
child.spawn_on_entity(world, child_entity).unwrap();
*existing_child_option = Some(child_entity);
world.get_mut::<ChildBlockPopulations>(parent).unwrap().0[block] = 1;
} else {
if let Some(existing_child) = existing_child_option.take() {
world.entity_mut(existing_child).despawn();
}
world.get_mut::<ChildBlockPopulations>(parent).unwrap().0[block] = 0;
}
};
let handle = child_option.map(system).register(world);
add_handles(world, parent, [handle]);
};
self.on_spawn(on_spawn)
}
pub fn children(self, children: impl IntoIterator<Item = impl Into<Builder>> + Send + 'static) -> Self {
let block = self.next_block.fetch_add(1, Ordering::Relaxed);
let children_vec: Vec<Builder> = children.into_iter().map(Into::into).collect();
let population = children_vec.len();
let on_spawn = move |world: &mut World, parent: Entity| {
let mut children_entities = Vec::with_capacity(children_vec.len());
for _ in 0..children_vec.len() {
children_entities.push(world.spawn_empty().id());
}
let mut pops = world.get_mut::<ChildBlockPopulations>(parent).unwrap();
pops.ensure_block(block);
pops.0[block] = population;
let insert_offset = offset(block, &pops.0);
world
.entity_mut(parent)
.insert_children(insert_offset, &children_entities);
for (child, child_entity) in children_vec.into_iter().zip(children_entities.iter().copied()) {
child.spawn_on_entity(world, child_entity).unwrap();
}
};
self.on_spawn(on_spawn)
}
pub fn children_signal_vec(self, children_signal_vec: impl SignalVec<Item = Builder>) -> Self {
let block = self.next_block.fetch_add(1, Ordering::Relaxed);
let on_spawn = move |world: &mut World, parent: Entity| {
let mut pops = world.get_mut::<ChildBlockPopulations>(parent).unwrap();
pops.ensure_block(block);
let system = move |In(diffs): In<Vec<VecDiff<Builder>>>,
world: &mut World,
mut children_entities: Local<Vec<Entity>>| {
for diff in diffs {
match diff {
VecDiff::Replace { values: children } => {
for child_entity in children_entities.drain(..) {
world.entity_mut(child_entity).despawn();
}
*children_entities = children.iter().map(|_| world.spawn_empty().id()).collect();
let pops = world.get::<ChildBlockPopulations>(parent).unwrap();
let insert_offset = offset(block, &pops.0);
world
.entity_mut(parent)
.insert_children(insert_offset, &children_entities);
for (child, child_entity) in children.into_iter().zip(children_entities.iter().copied()) {
child.spawn_on_entity(world, child_entity).unwrap();
}
world.get_mut::<ChildBlockPopulations>(parent).unwrap().0[block] = children_entities.len();
}
VecDiff::InsertAt { index, value: child } => {
let child_entity = world.spawn_empty().id();
let pops = world.get::<ChildBlockPopulations>(parent).unwrap();
let insert_offset = offset(block, &pops.0);
world
.entity_mut(parent)
.insert_children(insert_offset + index, &[child_entity]);
child.spawn_on_entity(world, child_entity).unwrap();
children_entities.insert(index, child_entity);
world.get_mut::<ChildBlockPopulations>(parent).unwrap().0[block] = children_entities.len();
}
VecDiff::Push { value: child } => {
let child_entity = world.spawn_empty().id();
let pops = world.get::<ChildBlockPopulations>(parent).unwrap();
let insert_offset = offset(block, &pops.0);
world
.entity_mut(parent)
.insert_children(insert_offset + children_entities.len(), &[child_entity]);
child.spawn_on_entity(world, child_entity).unwrap();
children_entities.push(child_entity);
world.get_mut::<ChildBlockPopulations>(parent).unwrap().0[block] = children_entities.len();
}
VecDiff::UpdateAt { index, value: node } => {
if let Some(existing_child) = children_entities.get(index).copied() {
world.entity_mut(existing_child).despawn(); }
let child_entity = world.spawn_empty().id();
let pops = world.get::<ChildBlockPopulations>(parent).unwrap();
let insert_offset = offset(block, &pops.0);
world
.entity_mut(parent)
.insert_children(insert_offset + index, &[child_entity]);
node.spawn_on_entity(world, child_entity).unwrap();
children_entities[index] = child_entity;
}
VecDiff::Move { old_index, new_index } => {
let moved_entity = children_entities.remove(old_index);
children_entities.insert(new_index, moved_entity);
let mut parent = world.entity_mut(parent);
parent.detach_children(&[moved_entity]);
let parent_entity = parent.id();
let pops = world.get::<ChildBlockPopulations>(parent_entity).unwrap();
let insert_offset = offset(block, &pops.0);
world
.entity_mut(parent_entity)
.insert_children(insert_offset + new_index, &[moved_entity]);
}
VecDiff::RemoveAt { index } => {
if let Some(existing_child) = children_entities.get(index).copied() {
world.entity_mut(existing_child).despawn();
children_entities.remove(index);
world.get_mut::<ChildBlockPopulations>(parent).unwrap().0[block] =
children_entities.len();
}
}
VecDiff::Pop => {
if let Some(child_entity) = children_entities.pop() {
world.entity_mut(child_entity).despawn();
world.get_mut::<ChildBlockPopulations>(parent).unwrap().0[block] =
children_entities.len();
}
}
VecDiff::Clear => {
for child_entity in children_entities.drain(..) {
world.entity_mut(child_entity).despawn();
}
world.get_mut::<ChildBlockPopulations>(parent).unwrap().0[block] = children_entities.len();
}
}
}
};
let handle = children_signal_vec.for_each(system).register(world);
add_handles(world, parent, [handle]);
};
self.on_spawn(on_spawn)
}
pub fn spawn_on_entity(self, world: &mut World, entity: Entity) -> Result<(), EntityMutableFetchError> {
let mut entity_mut = world.get_entity_mut(entity)?;
let id = entity_mut.id();
entity_mut.insert((SignalHandles::default(), ChildBlockPopulations::default()));
for on_spawn in self.on_spawns {
on_spawn(world, id);
}
Ok(())
}
pub fn spawn(self, world: &mut World) -> Entity {
let entity = world.spawn_empty().id();
self.spawn_on_entity(world, entity).unwrap();
entity
}
}
pub trait SignalTask: Send + Sync + 'static {
fn register_task(self: Box<Self>, world: &mut World) -> SignalHandle;
}
struct SignalTaskSignal<S>(S);
impl<S: Signal + Send + Sync + 'static> SignalTask for SignalTaskSignal<S> {
fn register_task(self: Box<Self>, world: &mut World) -> SignalHandle {
self.0.register(world)
}
}
pub trait SignalTaskExt: Signal + Sized + Send + Sync + 'static {
fn task(self) -> Box<dyn SignalTask> {
Box::new(SignalTaskSignal(self))
}
}
impl<S: Signal + Sized + Send + Sync + 'static> SignalTaskExt for S {}
struct SignalTaskSignalVec<S>(S);
impl<S: SignalVec + Send + Sync + 'static> SignalTask for SignalTaskSignalVec<S> {
fn register_task(self: Box<Self>, world: &mut World) -> SignalHandle {
self.0.register(world)
}
}
pub trait SignalVecTaskExt: SignalVec + Sized + Send + Sync + 'static {
fn task(self) -> Box<dyn SignalTask> {
Box::new(SignalTaskSignalVec(self))
}
}
impl<S: SignalVec + Sized + Send + Sync + 'static> SignalVecTaskExt for S {}
struct SignalTaskSignalMap<S>(S);
impl<S: SignalMap + Send + Sync + 'static> SignalTask for SignalTaskSignalMap<S> {
fn register_task(self: Box<Self>, world: &mut World) -> SignalHandle {
self.0.register(world)
}
}
pub trait SignalMapTaskExt: SignalMap + Sized + Send + Sync + 'static {
fn task(self) -> Box<dyn SignalTask> {
Box::new(SignalTaskSignalMap(self))
}
}
impl<S: SignalMap + Sized + Send + Sync + 'static> SignalMapTaskExt for S {}
fn on_despawn_hook(mut world: DeferredWorld, ctx: HookContext) {
let entity = ctx.entity;
let fs = world
.get_mut::<OnDespawnCallbacks>(entity)
.unwrap()
.0
.drain(..)
.collect::<Vec<_>>();
for f in fs {
f(&mut world, entity);
}
}
#[allow(clippy::type_complexity)]
#[derive(Component)]
#[component(on_remove = on_despawn_hook)]
struct OnDespawnCallbacks(Vec<Box<dyn FnOnce(&mut DeferredWorld, Entity) + Send + Sync + 'static>>);
fn add_handles<I>(world: &mut World, entity: Entity, handles: I)
where
I: IntoIterator<Item = SignalHandle>,
{
let mut entity = world.entity_mut(entity);
let mut existing_handles = entity.get_mut::<SignalHandles>().unwrap();
for handle in handles {
existing_handles.add(handle);
}
}
fn offset(i: usize, child_block_populations: &[usize]) -> usize {
child_block_populations[..i].iter().sum()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
self as jonmo, JonmoPlugin,
graph::STALE_SIGNALS,
signal::{self, SignalExt},
signal_vec::MutableVec,
};
use bevy::prelude::*;
use bevy_platform::{
collections::HashSet,
sync::{Arc, Mutex},
};
fn create_test_app() -> App {
cleanup();
let mut app = App::new();
app.add_plugins((MinimalPlugins, JonmoPlugin::default()));
app
}
fn cleanup() {
STALE_SIGNALS.lock().unwrap().clear();
crate::signal_vec::tests::cleanup(true);
}
#[test]
fn test_on_signal() {
let mut app = create_test_app();
#[derive(Resource, Default, Clone)]
struct TestOutput(Arc<Mutex<Vec<(Entity, i32)>>>);
app.init_resource::<TestOutput>();
fn capturing_system(In((entity, value_opt)): In<(Entity, Option<i32>)>, output: Res<TestOutput>) {
if let Some(value) = value_opt {
output.0.lock().unwrap().push((entity, value));
}
}
#[derive(Resource, Default, Clone, PartialEq)]
struct SignalSource(Option<i32>);
app.init_resource::<SignalSource>();
let source_signal = signal::from_resource::<SignalSource>()
.map_in(|source: SignalSource| source.0)
.dedupe();
let builder1 = jonmo::Builder::new()
.insert(Name::new("Entity 1"))
.on_signal(source_signal.clone(), capturing_system);
let entity1 = builder1.spawn(app.world_mut());
app.update();
assert!(app.world().resource::<TestOutput>().0.lock().unwrap().is_empty());
app.world_mut().resource_mut::<SignalSource>().0 = Some(100);
app.update();
let output_guard = app.world().resource::<TestOutput>().0.lock().unwrap();
assert_eq!(output_guard.len(), 1, "System should have run once.");
assert_eq!(
output_guard[0],
(entity1, 100),
"System received incorrect entity or value."
);
drop(output_guard);
app.world_mut().resource_mut::<TestOutput>().0.lock().unwrap().clear();
let builder2 = jonmo::Builder::new()
.insert(Name::new("Entity 2"))
.on_signal(source_signal.clone(), capturing_system);
let entity2 = builder2.spawn(app.world_mut());
app.world_mut().resource_mut::<SignalSource>().0 = Some(200);
app.update();
let output_guard = app.world().resource::<TestOutput>().0.lock().unwrap();
assert_eq!(output_guard.len(), 2, "Systems for both entities should have run.");
let received_set: HashSet<(Entity, i32)> = output_guard.iter().cloned().collect();
let expected_set: HashSet<(Entity, i32)> = [(entity1, 200), (entity2, 200)].into();
assert_eq!(
received_set, expected_set,
"Systems received incorrect parameters for multi-entity test."
);
drop(output_guard);
app.world_mut().resource_mut::<TestOutput>().0.lock().unwrap().clear();
app.world_mut().entity_mut(entity1).despawn();
app.update();
app.world_mut().resource_mut::<SignalSource>().0 = Some(300);
app.update();
let output_guard = app.world().resource::<TestOutput>().0.lock().unwrap();
assert_eq!(
output_guard.len(),
1,
"Only the system for the remaining entity should have run."
);
assert_eq!(
output_guard[0],
(entity2, 300),
"The remaining system received incorrect parameters."
);
drop(output_guard);
assert!(
app.world().get_entity(entity1).is_err(),
"Despawned entity should not exist."
);
app.world_mut().entity_mut(entity2).despawn();
app.update();
app.world_mut().resource_mut::<TestOutput>().0.lock().unwrap().clear();
app.world_mut().resource_mut::<SignalSource>().0 = Some(400);
app.update();
let output_guard = app.world().resource::<TestOutput>().0.lock().unwrap();
assert!(
output_guard.is_empty(),
"No systems should run after all entities are despawned."
);
}
#[test]
fn test_on_signal_with_component() {
let mut app = create_test_app();
#[derive(Component, Clone, Debug, PartialEq)]
struct TestComponent(i32);
#[derive(Resource, Default, Clone, PartialEq)]
struct SignalSource(i32);
app.init_resource::<SignalSource>();
let source_signal = signal::from_resource::<SignalSource>()
.map_in(|source: SignalSource| source.0)
.dedupe();
let mutator_closure = |mut component: Mut<TestComponent>, value: i32| {
component.0 += value;
};
let builder1 = jonmo::Builder::new()
.insert(TestComponent(10))
.on_signal_with_component(source_signal.clone(), mutator_closure);
let entity1 = builder1.spawn(app.world_mut());
app.world_mut().resource_mut::<SignalSource>().0 = 5;
app.update();
let component1 = app.world().get::<TestComponent>(entity1).unwrap();
assert_eq!(component1.0, 15, "Component should be 10 + 5 = 15.");
let builder_no_comp = jonmo::Builder::new()
.on_signal_with_component(source_signal.clone(), mutator_closure);
let entity_no_comp = builder_no_comp.spawn(app.world_mut());
app.world_mut().resource_mut::<SignalSource>().0 = 10;
app.update();
assert!(
app.world().get::<TestComponent>(entity_no_comp).is_none(),
"Entity should not have TestComponent added to it."
);
let component1 = app.world().get::<TestComponent>(entity1).unwrap();
assert_eq!(component1.0, 25, "Existing entity should be updated (15 + 10).");
let builder2 = jonmo::Builder::new()
.insert(TestComponent(100))
.on_signal_with_component(source_signal.clone(), mutator_closure);
let entity2 = builder2.spawn(app.world_mut());
app.world_mut().resource_mut::<SignalSource>().0 = 20;
app.update();
let component1 = app.world().get::<TestComponent>(entity1).unwrap();
assert_eq!(component1.0, 45, "Entity 1 should be 25 + 20 = 45.");
let component2 = app.world().get::<TestComponent>(entity2).unwrap();
assert_eq!(component2.0, 120, "Entity 2 should be 100 + 20 = 120.");
app.world_mut().entity_mut(entity1).despawn();
app.update();
app.world_mut().resource_mut::<SignalSource>().0 = 30;
app.update();
assert!(
app.world().get_entity(entity1).is_err(),
"Despawned entity should not exist."
);
let component2 = app.world().get::<TestComponent>(entity2).unwrap();
assert_eq!(component2.0, 150, "Only Entity 2 should be updated (120 + 30).");
}
#[test]
fn test_component_signal() {
let mut app = create_test_app();
#[derive(Component, Clone, Debug, PartialEq)]
struct TargetComponent(String);
#[derive(Resource, Default, Clone, PartialEq)]
struct SignalSource(Option<String>);
app.init_resource::<SignalSource>();
let source_signal = signal::from_resource::<SignalSource>()
.dedupe()
.map_in(|source: SignalSource| {
source.0.map(TargetComponent) });
let builder = jonmo::Builder::new().component_signal(source_signal.clone());
let entity1 = builder.spawn(app.world_mut());
assert!(app.world().get::<TargetComponent>(entity1).is_none());
app.world_mut().resource_mut::<SignalSource>().0 = Some("Initial".to_string());
app.update();
assert_eq!(
app.world().get::<TargetComponent>(entity1),
Some(&TargetComponent("Initial".to_string())),
"Component should be inserted on first Some signal."
);
app.world_mut().resource_mut::<SignalSource>().0 = Some("Updated".to_string());
app.update();
assert_eq!(
app.world().get::<TargetComponent>(entity1),
Some(&TargetComponent("Updated".to_string())),
"Component should be updated on subsequent Some signal."
);
app.world_mut().resource_mut::<SignalSource>().0 = None;
app.update();
assert!(
app.world().get::<TargetComponent>(entity1).is_none(),
"Component should be removed on None signal."
);
app.world_mut().resource_mut::<SignalSource>().0 = Some("Reinserted".to_string());
app.update();
assert_eq!(
app.world().get::<TargetComponent>(entity1),
Some(&TargetComponent("Reinserted".to_string())),
"Component should be re-inserted after being removed."
);
let builder2 = jonmo::Builder::new().component_signal(source_signal.clone());
let entity2 = builder2.spawn(app.world_mut());
assert!(app.world().get::<TargetComponent>(entity2).is_none());
app.world_mut().resource_mut::<SignalSource>().0 = Some("Multi".to_string());
app.update();
assert_eq!(
app.world().get::<TargetComponent>(entity1),
Some(&TargetComponent("Multi".to_string())),
"Entity 1 should be updated in multi-entity test."
);
assert_eq!(
app.world().get::<TargetComponent>(entity2),
Some(&TargetComponent("Multi".to_string())),
"Entity 2 should be updated in multi-entity test."
);
app.world_mut().entity_mut(entity1).despawn();
app.update();
app.world_mut().resource_mut::<SignalSource>().0 = Some("Post-Despawn".to_string());
app.update();
assert_eq!(
app.world().get::<TargetComponent>(entity2),
Some(&TargetComponent("Post-Despawn".to_string())),
"Only the remaining entity should have been updated."
);
assert!(
app.world().get_entity(entity1).is_err(),
"Despawned entity should not exist."
);
}
#[test]
fn test_child() {
{
let mut app = create_test_app();
#[derive(Component, Debug, PartialEq)]
struct ParentComp;
#[derive(Component, Debug, PartialEq)]
struct ChildCompA;
#[derive(Component, Debug, PartialEq)]
struct ChildCompB;
#[derive(Component, Debug, PartialEq)]
struct ChildCompC;
#[derive(Component, Debug, PartialEq)]
struct ChildCompD;
#[derive(Component, Debug, PartialEq)]
struct ChildCompE;
#[derive(Component, Clone, Debug, PartialEq)]
struct SourceComp(i32);
#[derive(Component, Clone, Debug, PartialEq)]
struct TargetComp(i32);
let child_builder_simple = jonmo::Builder::new().insert((ChildCompA, Name::new("SimpleChild")));
let parent_builder_simple = jonmo::Builder::new().insert(ParentComp).child(child_builder_simple);
let parent_entity_simple = parent_builder_simple.spawn(app.world_mut());
app.update();
let mut children_query = app.world_mut().query::<&Children>();
let parent_children = children_query.get(app.world(), parent_entity_simple).unwrap();
assert_eq!(parent_children.len(), 1, "Parent should have exactly one child.");
let child_entity_simple = parent_children[0];
let child_entity_ref = app.world().entity(child_entity_simple);
assert!(child_entity_ref.contains::<ChildCompA>());
assert_eq!(child_entity_ref.get::<Name>().unwrap().as_str(), "SimpleChild");
let mut parent_query = app.world_mut().query::<&bevy::prelude::ChildOf>();
let childs_parent = parent_query.get(app.world(), child_entity_simple).unwrap();
assert_eq!(childs_parent.parent(), parent_entity_simple);
app.world_mut().entity_mut(parent_entity_simple).despawn();
app.update();
let parent_builder_chained = jonmo::Builder::new()
.insert(ParentComp)
.child(jonmo::Builder::new().insert(ChildCompA))
.child(jonmo::Builder::new().insert(ChildCompB));
let parent_entity_chained = parent_builder_chained.spawn(app.world_mut());
app.update();
let children = app.world().get::<Children>(parent_entity_chained).unwrap();
assert_eq!(children.len(), 2, "Parent should have two children.");
let child_a_entity = children[0];
let child_b_entity = children[1];
assert!(
app.world().entity(child_a_entity).contains::<ChildCompA>(),
"First child should be A"
);
assert!(
app.world().entity(child_b_entity).contains::<ChildCompB>(),
"Second child should be B"
);
app.world_mut().entity_mut(parent_entity_chained).despawn();
app.update();
let grandchild_builder = jonmo::Builder::new().insert(Name::new("Grandchild"));
let child_builder_nested = jonmo::Builder::new()
.insert(Name::new("Child"))
.child(grandchild_builder);
let grandparent_builder = jonmo::Builder::new()
.insert(Name::new("Grandparent"))
.child(child_builder_nested);
let grandparent_entity = grandparent_builder.spawn(app.world_mut());
app.update();
let gp_children = app.world().get::<Children>(grandparent_entity).unwrap();
assert_eq!(gp_children.len(), 1);
let child_entity_nested = gp_children[0];
assert_eq!(app.world().get::<Name>(child_entity_nested).unwrap().as_str(), "Child");
let child_children = app.world().get::<Children>(child_entity_nested).unwrap();
assert_eq!(child_children.len(), 1);
let grandchild_entity = child_children[0];
assert_eq!(
app.world().get::<Name>(grandchild_entity).unwrap().as_str(),
"Grandchild"
);
app.world_mut().entity_mut(grandparent_entity).despawn();
app.update();
#[derive(Resource, Default, Deref, DerefMut, Clone)]
struct SignalTrigger(bool);
app.init_resource::<SignalTrigger>();
let child_a = jonmo::Builder::new().insert(ChildCompA);
let child_b = jonmo::Builder::new().insert(ChildCompB);
let child_c = jonmo::Builder::new().insert(ChildCompC);
let child_d = jonmo::Builder::new().insert(ChildCompD);
fn make_child_e() -> jonmo::Builder {
jonmo::Builder::new().insert(ChildCompE)
}
let parent_builder_mixed = jonmo::Builder::new()
.insert(ParentComp)
.child(child_a) .children([child_b, child_c]) .child(child_d) .child_signal(
signal::from_resource::<SignalTrigger>().map_in::<Option<Builder>, _, _>(
move |trigger: SignalTrigger| if trigger.0 { Some(make_child_e()) } else { None },
),
);
let parent_entity_mixed = parent_builder_mixed.spawn(app.world_mut());
app.update();
let children_mixed = app.world().get::<Children>(parent_entity_mixed).unwrap();
assert_eq!(children_mixed.len(), 4, "Should have 4 children initially.");
assert!(app.world().entity(children_mixed[0]).contains::<ChildCompA>());
assert!(app.world().entity(children_mixed[1]).contains::<ChildCompB>());
assert!(app.world().entity(children_mixed[2]).contains::<ChildCompC>());
assert!(app.world().entity(children_mixed[3]).contains::<ChildCompD>());
app.world_mut().resource_mut::<SignalTrigger>().0 = true;
app.update();
let children_mixed_after = app.world().get::<Children>(parent_entity_mixed).unwrap();
assert_eq!(
children_mixed_after.len(),
5,
"Should have 5 children after signal trigger."
);
assert!(app.world().entity(children_mixed_after[0]).contains::<ChildCompA>());
assert!(app.world().entity(children_mixed_after[1]).contains::<ChildCompB>());
assert!(app.world().entity(children_mixed_after[2]).contains::<ChildCompC>());
assert!(app.world().entity(children_mixed_after[3]).contains::<ChildCompD>());
assert!(
app.world().entity(children_mixed_after[4]).contains::<ChildCompE>(),
"Child E should be last."
);
app.world_mut().entity_mut(parent_entity_mixed).despawn();
app.update();
let child_entity = LazyEntity::new();
let child_builder_with_signal = jonmo::Builder::new().lazy_entity(child_entity.clone()).on_signal(
signal::from_parent(child_entity.clone())
.component::<SourceComp>()
.map_in(|source: SourceComp| Some(TargetComp(source.0 * 2))),
|In((entity, comp_opt)): In<(Entity, Option<TargetComp>)>, world: &mut World| {
if let Ok(mut e) = world.get_entity_mut(entity) {
if let Some(comp) = comp_opt {
e.insert(comp);
} else {
e.remove::<TargetComp>();
}
}
},
);
let parent_builder_with_signal = jonmo::Builder::new()
.insert(SourceComp(10))
.child(child_builder_with_signal);
let parent_entity_signal = parent_builder_with_signal.spawn(app.world_mut());
app.update();
let child_entity_signal = app.world().get::<Children>(parent_entity_signal).unwrap()[0];
let child_target_comp = app.world().get::<TargetComp>(child_entity_signal);
assert_eq!(
child_target_comp,
Some(&TargetComp(20)),
"Child's signal did not correctly read parent component and update itself."
);
app.world_mut().get_mut::<SourceComp>(parent_entity_signal).unwrap().0 = 50;
app.update();
let child_target_comp_updated = app.world().get::<TargetComp>(child_entity_signal);
assert_eq!(
child_target_comp_updated,
Some(&TargetComp(100)),
"Child's signal did not react to parent component change."
);
app.world_mut().entity_mut(parent_entity_signal).despawn();
app.update();
}
cleanup()
}
#[derive(Component, Debug, PartialEq)]
struct ParentComp;
#[derive(Component, Debug, PartialEq)]
struct StaticChildBefore;
#[derive(Component, Debug, PartialEq)]
struct StaticChildAfter;
#[derive(Component, Debug, PartialEq)]
struct ReactiveChild(u32);
#[derive(Resource, Default, Clone, PartialEq)]
struct SignalSource(Option<u32>);
fn get_child_types(world: &mut World, parent: Entity) -> Vec<String> {
let mut children_query = world.query_filtered::<&Children, With<ParentComp>>();
let Ok(children) = children_query.get(world, parent) else {
return vec![];
};
children
.iter()
.map(|child_entity| {
let entity_ref = world.entity(child_entity);
if entity_ref.contains::<StaticChildBefore>() {
"Before".to_string()
} else if entity_ref.contains::<StaticChildAfter>() {
"After".to_string()
} else if let Some(rc) = entity_ref.get::<ReactiveChild>() {
format!("Reactive({})", rc.0)
} else {
"Unknown".to_string()
}
})
.collect()
}
#[test]
fn test_child_signal() {
{
let mut app = create_test_app();
app.init_resource::<SignalSource>();
let reactive_child_factory = |id: u32| jonmo::Builder::new().insert(ReactiveChild(id));
let source_signal = signal::from_resource::<SignalSource>()
.dedupe()
.map_in(move |source: SignalSource| source.0.map(reactive_child_factory));
let parent_builder = jonmo::Builder::new()
.insert(ParentComp)
.child(jonmo::Builder::new().insert(StaticChildBefore)) .child_signal(source_signal) .child(jonmo::Builder::new().insert(StaticChildAfter));
let parent_entity = parent_builder.spawn(app.world_mut());
app.update();
assert_eq!(
get_child_types(app.world_mut(), parent_entity),
vec!["Before", "After"],
"Initial state with None should only have static children"
);
assert_eq!(app.world().get::<Children>(parent_entity).unwrap().len(), 2);
app.world_mut().resource_mut::<SignalSource>().0 = Some(1);
app.update();
assert_eq!(
get_child_types(app.world_mut(), parent_entity),
vec!["Before", "Reactive(1)", "After"],
"Transition None->Some failed to create and order child correctly"
);
assert_eq!(app.world().get::<Children>(parent_entity).unwrap().len(), 3);
app.update();
assert_eq!(
get_child_types(app.world_mut(), parent_entity),
vec!["Before", "Reactive(1)", "After"],
"No-op update should not change children"
);
assert_eq!(app.world().get::<Children>(parent_entity).unwrap().len(), 3);
let old_child_entity = app.world().get::<Children>(parent_entity).unwrap()[1];
app.world_mut().resource_mut::<SignalSource>().0 = Some(2);
app.update();
assert_eq!(
get_child_types(app.world_mut(), parent_entity),
vec!["Before", "Reactive(2)", "After"],
"Transition Some->Some failed to replace and order child correctly"
);
assert_eq!(app.world().get::<Children>(parent_entity).unwrap().len(), 3);
assert!(
app.world().get_entity(old_child_entity).is_err(),
"Old reactive child was not despawned on replacement"
);
let old_child_entity = app.world().get::<Children>(parent_entity).unwrap()[1];
app.world_mut().resource_mut::<SignalSource>().0 = None;
app.update();
assert_eq!(
get_child_types(app.world_mut(), parent_entity),
vec!["Before", "After"],
"Transition Some->None failed to remove child"
);
assert_eq!(app.world().get::<Children>(parent_entity).unwrap().len(), 2);
assert!(
app.world().get_entity(old_child_entity).is_err(),
"Reactive child was not despawned on transition to None"
);
app.world_mut().resource_mut::<SignalSource>().0 = Some(3);
app.update();
assert_eq!(
get_child_types(app.world_mut(), parent_entity),
vec!["Before", "Reactive(3)", "After"],
"Transition back to Some failed"
);
assert_eq!(app.world().get::<Children>(parent_entity).unwrap().len(), 3);
let child_to_despawn = app.world().get::<Children>(parent_entity).unwrap()[1];
app.world_mut().entity_mut(parent_entity).despawn();
app.update();
assert!(
app.world().get_entity(parent_entity).is_err(),
"Parent should be despawned."
);
assert!(
app.world().get_entity(child_to_despawn).is_err(),
"Reactive child should be despawned with parent."
);
app.world_mut().resource_mut::<SignalSource>().0 = Some(4);
app.update();
let mut reactive_children_query = app.world_mut().query::<&ReactiveChild>();
assert_eq!(
reactive_children_query.iter(app.world()).count(),
0,
"No reactive children should exist after parent is despawned."
);
}
cleanup()
}
#[test]
fn test_children() {
{
let mut app = create_test_app();
#[derive(Component, Debug, PartialEq)]
struct ParentComp;
#[derive(Component, Debug, PartialEq)]
struct ChildCompA;
#[derive(Component, Debug, PartialEq)]
struct ChildCompB;
#[derive(Component, Debug, PartialEq)]
struct ChildCompC;
#[derive(Component, Debug, PartialEq)]
struct ChildCompD;
#[derive(Component, Debug, PartialEq)]
struct ChildCompE;
#[derive(Component, Debug, PartialEq)]
struct ChildCompF;
#[derive(Component, Debug, PartialEq)]
struct ChildCompG;
#[derive(Component, Debug, PartialEq)]
struct ChildCompH;
#[derive(Component, Debug, PartialEq)]
struct GrandchildComp;
#[derive(Component, Clone, Debug, PartialEq)]
struct SourceComp(i32);
#[derive(Component, Clone, Debug, PartialEq)]
struct TargetComp(i32);
{
let child_builders = vec![
jonmo::Builder::new().insert(ChildCompA),
jonmo::Builder::new().insert(ChildCompB),
jonmo::Builder::new().insert(ChildCompC),
];
let parent_builder = jonmo::Builder::new().insert(ParentComp).children(child_builders);
let parent_entity = parent_builder.spawn(app.world_mut());
app.update();
let children = app
.world()
.get::<Children>(parent_entity)
.expect("Parent should have Children component.")
.iter()
.collect::<Vec<_>>();
assert_eq!(children.len(), 3, "Parent should have exactly 3 children.");
assert!(
app.world().entity(children[0]).contains::<ChildCompA>(),
"Child 0 should be A"
);
assert!(
app.world().entity(children[1]).contains::<ChildCompB>(),
"Child 1 should be B"
);
assert!(
app.world().entity(children[2]).contains::<ChildCompC>(),
"Child 2 should be C"
);
let mut parent_rel_query = app.world_mut().query::<&bevy::prelude::ChildOf>();
for child_entity in children.into_iter() {
assert_eq!(
parent_rel_query.get(app.world(), child_entity).unwrap().parent(),
parent_entity
);
}
app.world_mut().entity_mut(parent_entity).despawn();
app.update();
}
{
let parent_builder_empty = jonmo::Builder::new()
.insert(ParentComp)
.children(vec![] as Vec<Builder>);
let parent_entity_empty = parent_builder_empty.spawn(app.world_mut());
app.update();
assert!(
app.world().get::<Children>(parent_entity_empty).is_none(),
"Parent with empty children iterator should not have a Children component."
);
app.world_mut().entity_mut(parent_entity_empty).despawn();
app.update();
}
{
let complex_child_entity = LazyEntity::new();
let complex_child_builder = jonmo::Builder::new()
.lazy_entity(complex_child_entity.clone())
.insert(ChildCompD)
.child(jonmo::Builder::new().insert(GrandchildComp)) .on_signal(
signal::from_parent(complex_child_entity.clone())
.component::<SourceComp>()
.map_in(|source: SourceComp| Some(TargetComp(source.0 * 10))),
|In((entity, comp_opt)): In<(Entity, Option<TargetComp>)>, world: &mut World| {
if let Ok(mut e) = world.get_entity_mut(entity) {
if let Some(comp) = comp_opt {
e.insert(comp);
} else {
e.remove::<TargetComp>();
}
}
},
);
let parent_builder_complex = jonmo::Builder::new()
.insert((ParentComp, SourceComp(5)))
.children(vec![complex_child_builder]);
let parent_entity_complex = parent_builder_complex.spawn(app.world_mut());
app.update();
let complex_children = app.world().get::<Children>(parent_entity_complex).unwrap();
let complex_child_entity_id = complex_children[0];
assert_eq!(
app.world().get::<TargetComp>(complex_child_entity_id),
Some(&TargetComp(50))
);
let grandchild_entity = app.world().get::<Children>(complex_child_entity_id).unwrap()[0];
assert!(app.world().entity(grandchild_entity).contains::<GrandchildComp>());
app.world_mut().get_mut::<SourceComp>(parent_entity_complex).unwrap().0 = 7;
app.update();
assert_eq!(
app.world().get::<TargetComp>(complex_child_entity_id),
Some(&TargetComp(70)),
"Signal did not react to parent's component change."
);
app.world_mut().entity_mut(parent_entity_complex).despawn();
app.update();
assert!(
app.world().get_entity(complex_child_entity_id).is_err(),
"Complex child should be despawned with parent."
);
assert!(
app.world().get_entity(grandchild_entity).is_err(),
"Grandchild should be despawned with parent."
);
}
{
fn get_ordered_child_markers(world: &World, parent: Entity) -> Vec<&'static str> {
let Some(children) = world.get::<Children>(parent) else {
return vec![];
};
children
.iter()
.map(|child_entity| {
let e = world.entity(child_entity);
if e.contains::<ChildCompA>() {
"A"
} else if e.contains::<ChildCompB>() {
"B"
} else if e.contains::<ChildCompC>() {
"C"
} else if e.contains::<ChildCompD>() {
"D"
} else if e.contains::<ChildCompE>() {
"E"
} else if e.contains::<ChildCompF>() {
"F"
} else if e.contains::<ChildCompG>() {
"G"
} else if e.contains::<ChildCompH>() {
"H"
} else {
"Unknown"
}
})
.collect()
}
#[derive(Resource, Default, Deref, DerefMut, Clone)]
struct SignalTrigger(bool);
app.init_resource::<SignalTrigger>();
let parent_builder = jonmo::Builder::new()
.insert(ParentComp)
.child(jonmo::Builder::new().insert(ChildCompA)) .children([
jonmo::Builder::new().insert(ChildCompB),
jonmo::Builder::new().insert(ChildCompC),
])
.child_signal(
signal::from_resource::<SignalTrigger>().map_in(|trigger: SignalTrigger| {
if trigger.0 {
Some(jonmo::Builder::new().insert(ChildCompD))
} else {
None
}
}),
)
.children(vec![
jonmo::Builder::new().insert(ChildCompE),
jonmo::Builder::new().insert(ChildCompF),
jonmo::Builder::new().insert(ChildCompG),
])
.child(jonmo::Builder::new().insert(ChildCompH));
let parent_entity = parent_builder.spawn(app.world_mut());
app.update();
assert_eq!(
get_ordered_child_markers(app.world(), parent_entity),
vec!["A", "B", "C", "E", "F", "G", "H"],
"Initial mixed child order is incorrect"
);
assert_eq!(app.world().get::<Children>(parent_entity).unwrap().len(), 7);
app.world_mut().resource_mut::<SignalTrigger>().0 = true;
app.update();
assert_eq!(
get_ordered_child_markers(app.world(), parent_entity),
vec!["A", "B", "C", "D", "E", "F", "G", "H"],
"Mixed child order after signal trigger is incorrect"
);
assert_eq!(app.world().get::<Children>(parent_entity).unwrap().len(), 8);
app.world_mut().resource_mut::<SignalTrigger>().0 = false;
app.update();
assert_eq!(
get_ordered_child_markers(app.world(), parent_entity),
vec!["A", "B", "C", "E", "F", "G", "H"],
"Mixed child order after signal un-triggers is incorrect"
);
assert_eq!(app.world().get::<Children>(parent_entity).unwrap().len(), 7);
}
}
cleanup()
}
fn get_children_entities(world: &mut World, parent: Entity) -> Vec<Entity> {
world
.get::<Children>(parent)
.map(|children| children.iter().collect())
.unwrap_or_default()
}
fn get_all_child_types(world: &mut World, parent: Entity) -> Vec<String> {
let Ok(children) = world.query::<&Children>().get(world, parent) else {
return vec![];
};
children
.iter()
.map(|child_entity| {
let entity_ref = world.entity(child_entity);
if entity_ref.contains::<StaticChildBefore>() {
"StaticBefore".to_string()
} else if entity_ref.contains::<StaticChildAfter>() {
"StaticAfter".to_string()
} else if let Some(rc) = entity_ref.get::<ReactiveChild>() {
format!("Reactive({})", rc.0)
} else {
"Unknown".to_string()
}
})
.collect()
}
#[test]
fn test_children_signal_vec() {
{
let mut app = create_test_app();
let source_vec = MutableVec::builder().values([10u32, 20u32]).spawn(app.world_mut());
let child_builder_factory = |id: u32| jonmo::Builder::new().insert(ReactiveChild(id));
let children_signal = source_vec.signal_vec().map_in(child_builder_factory);
let parent_builder = jonmo::Builder::new()
.insert(ParentComp)
.child(jonmo::Builder::new().insert(StaticChildBefore))
.children_signal_vec(children_signal)
.child(jonmo::Builder::new().insert(StaticChildAfter));
let parent_entity = parent_builder.spawn(app.world_mut());
app.update();
assert_eq!(
get_all_child_types(app.world_mut(), parent_entity),
vec!["StaticBefore", "Reactive(10)", "Reactive(20)", "StaticAfter"],
"Initial child order and content is incorrect."
);
source_vec.write(app.world_mut()).push(30);
app.update();
assert_eq!(
get_all_child_types(app.world_mut(), parent_entity),
vec![
"StaticBefore",
"Reactive(10)",
"Reactive(20)",
"Reactive(30)",
"StaticAfter"
],
"State after Push is incorrect."
);
source_vec.write(app.world_mut()).insert(1, 15); app.update();
assert_eq!(
get_all_child_types(app.world_mut(), parent_entity),
vec![
"StaticBefore",
"Reactive(10)",
"Reactive(15)",
"Reactive(20)",
"Reactive(30)",
"StaticAfter"
],
"State after InsertAt is incorrect."
);
let old_child_entities = get_children_entities(app.world_mut(), parent_entity);
let entity_to_be_replaced = old_child_entities[3]; source_vec.write(app.world_mut()).set(2, 25); app.update();
assert_eq!(
get_all_child_types(app.world_mut(), parent_entity),
vec![
"StaticBefore",
"Reactive(10)",
"Reactive(15)",
"Reactive(25)",
"Reactive(30)",
"StaticAfter"
],
"State after UpdateAt is incorrect."
);
assert!(
app.world().get_entity(entity_to_be_replaced).is_err(),
"Old child entity should be despawned after UpdateAt."
);
let old_child_entities = get_children_entities(app.world_mut(), parent_entity);
let entity_to_be_removed = old_child_entities[2]; source_vec.write(app.world_mut()).remove(1); app.update();
assert_eq!(
get_all_child_types(app.world_mut(), parent_entity),
vec![
"StaticBefore",
"Reactive(10)",
"Reactive(25)",
"Reactive(30)",
"StaticAfter"
],
"State after RemoveAt is incorrect."
);
assert!(
app.world().get_entity(entity_to_be_removed).is_err(),
"Child entity should be despawned after RemoveAt."
);
source_vec.write(app.world_mut()).move_item(2, 0); app.update();
assert_eq!(
get_all_child_types(app.world_mut(), parent_entity),
vec![
"StaticBefore",
"Reactive(30)",
"Reactive(10)",
"Reactive(25)",
"StaticAfter"
],
"State after Move is incorrect."
);
let old_child_entities = get_children_entities(app.world_mut(), parent_entity);
let entity_to_be_popped = old_child_entities[3]; source_vec.write(app.world_mut()).pop(); app.update();
assert_eq!(
get_all_child_types(app.world_mut(), parent_entity),
vec!["StaticBefore", "Reactive(30)", "Reactive(10)", "StaticAfter"],
"State after Pop is incorrect."
);
assert!(
app.world().get_entity(entity_to_be_popped).is_err(),
"Child entity should be despawned after Pop."
);
let reactive_children_before_clear = get_children_entities(app.world_mut(), parent_entity)
.into_iter()
.filter(|e| app.world().get::<ReactiveChild>(*e).is_some())
.collect::<Vec<_>>();
source_vec.write(app.world_mut()).clear();
app.update();
assert_eq!(
get_all_child_types(app.world_mut(), parent_entity),
vec!["StaticBefore", "StaticAfter"],
"State after Clear is incorrect."
);
for child in reactive_children_before_clear {
assert!(
app.world().get_entity(child).is_err(),
"All reactive children should be despawned after Clear."
);
}
source_vec.write(app.world_mut()).replace(vec![100, 200]);
app.update();
assert_eq!(
get_all_child_types(app.world_mut(), parent_entity),
vec!["StaticBefore", "Reactive(100)", "Reactive(200)", "StaticAfter"],
"State after Replace is incorrect."
);
let all_children_before_despawn = get_children_entities(app.world_mut(), parent_entity);
app.world_mut().entity_mut(parent_entity).despawn();
app.update();
assert!(
app.world().get_entity(parent_entity).is_err(),
"Parent entity should be despawned."
);
for child in all_children_before_despawn {
assert!(
app.world().get_entity(child).is_err(),
"All children (static and reactive) should be despawned with parent."
);
}
source_vec.write(app.world_mut()).push(999);
app.update();
}
cleanup()
}
#[test]
fn test_on_despawn() {
let mut app = create_test_app();
#[derive(Resource, Default, Clone)]
struct DespawnTracker(Arc<Mutex<Vec<(Entity, String)>>>);
app.init_resource::<DespawnTracker>();
let tracker = app.world().resource::<DespawnTracker>().clone();
let builder1 = jonmo::Builder::new().on_despawn({
let tracker = tracker.clone();
move |_world, entity| {
tracker.0.lock().unwrap().push((entity, "callback1".to_string()));
}
});
let entity1 = builder1.spawn(app.world_mut());
app.update();
assert!(
tracker.0.lock().unwrap().is_empty(),
"on_despawn callback should not run before despawn."
);
app.world_mut().entity_mut(entity1).despawn();
app.update();
let tracker_guard = tracker.0.lock().unwrap();
assert_eq!(tracker_guard.len(), 1, "Callback should have been called exactly once.");
assert_eq!(tracker_guard[0], (entity1, "callback1".to_string()));
drop(tracker_guard);
tracker.0.lock().unwrap().clear();
let builder2 = jonmo::Builder::new()
.on_despawn({
let tracker = tracker.clone();
move |_world, entity| {
tracker.0.lock().unwrap().push((entity, "first".to_string()));
}
})
.on_despawn({
let tracker = tracker.clone();
move |_world, entity| {
tracker.0.lock().unwrap().push((entity, "second".to_string()));
}
})
.on_despawn({
let tracker = tracker.clone();
move |_world, entity| {
tracker.0.lock().unwrap().push((entity, "third".to_string()));
}
});
let entity2 = builder2.spawn(app.world_mut());
app.update();
app.world_mut().entity_mut(entity2).despawn();
app.update();
let tracker_guard = tracker.0.lock().unwrap();
assert_eq!(tracker_guard.len(), 3, "All three callbacks should have been called.");
assert_eq!(tracker_guard[0], (entity2, "first".to_string()));
assert_eq!(tracker_guard[1], (entity2, "second".to_string()));
assert_eq!(tracker_guard[2], (entity2, "third".to_string()));
drop(tracker_guard);
tracker.0.lock().unwrap().clear();
let builder3 = jonmo::Builder::new().on_despawn({
let tracker = tracker.clone();
move |_world, entity| {
tracker.0.lock().unwrap().push((entity, "entity3".to_string()));
}
});
let entity3 = builder3.spawn(app.world_mut());
let builder4 = jonmo::Builder::new().on_despawn({
let tracker = tracker.clone();
move |_world, entity| {
tracker.0.lock().unwrap().push((entity, "entity4".to_string()));
}
});
let entity4 = builder4.spawn(app.world_mut());
app.update();
app.world_mut().entity_mut(entity3).despawn();
app.update();
let tracker_guard = tracker.0.lock().unwrap();
assert_eq!(tracker_guard.len(), 1, "Only one callback should have been called.");
assert_eq!(tracker_guard[0], (entity3, "entity3".to_string()));
drop(tracker_guard);
tracker.0.lock().unwrap().clear();
app.world_mut().entity_mut(entity4).despawn();
app.update();
let tracker_guard = tracker.0.lock().unwrap();
assert_eq!(tracker_guard.len(), 1, "entity4's callback should have been called.");
assert_eq!(tracker_guard[0], (entity4, "entity4".to_string()));
drop(tracker_guard);
tracker.0.lock().unwrap().clear();
#[derive(Resource, Default)]
struct DespawnCounter(u32);
app.init_resource::<DespawnCounter>();
let builder5 = jonmo::Builder::new().on_despawn(|world, _entity| {
world.resource_mut::<DespawnCounter>().0 += 1;
});
let entity5 = builder5.spawn(app.world_mut());
app.update();
assert_eq!(
app.world().resource::<DespawnCounter>().0,
0,
"Counter should be 0 before despawn."
);
app.world_mut().entity_mut(entity5).despawn();
app.update();
assert_eq!(
app.world().resource::<DespawnCounter>().0,
1,
"Counter should be incremented by on_despawn callback."
);
let builder6 = jonmo::Builder::new().on_despawn(|world, _entity| {
world.resource_mut::<DespawnCounter>().0 += 10;
});
let entity6 = builder6.spawn(app.world_mut());
app.update();
app.world_mut().entity_mut(entity6).despawn();
app.update();
assert_eq!(
app.world().resource::<DespawnCounter>().0,
11,
"Counter should be 1 + 10 = 11 after second despawn."
);
#[derive(Component)]
struct TestMarker;
let child_despawn_tracker = Arc::new(Mutex::new(Vec::new()));
let parent_despawn_tracker = Arc::new(Mutex::new(Vec::new()));
let child_tracker = child_despawn_tracker.clone();
let parent_tracker = parent_despawn_tracker.clone();
let builder_parent = jonmo::Builder::new()
.insert(TestMarker)
.on_despawn(move |_world, entity| {
parent_tracker.lock().unwrap().push(entity);
})
.child(jonmo::Builder::new().on_despawn(move |_world, entity| {
child_tracker.lock().unwrap().push(entity);
}));
let parent_entity = builder_parent.spawn(app.world_mut());
app.update();
let children: Vec<Entity> = app
.world()
.get::<Children>(parent_entity)
.map(|c| c.iter().collect())
.unwrap_or_default();
assert_eq!(children.len(), 1, "Parent should have one child.");
let child_entity = children[0];
app.world_mut().entity_mut(parent_entity).despawn();
app.update();
assert_eq!(
parent_despawn_tracker.lock().unwrap().len(),
1,
"Parent's on_despawn should have been called."
);
assert_eq!(
parent_despawn_tracker.lock().unwrap()[0],
parent_entity,
"Parent callback should receive parent entity."
);
assert_eq!(
child_despawn_tracker.lock().unwrap().len(),
1,
"Child's on_despawn should have been called when parent is despawned."
);
assert_eq!(
child_despawn_tracker.lock().unwrap()[0],
child_entity,
"Child callback should receive child entity."
);
}
}