From a0dbfe0e51ae6a245a6140e52df8d8ba20be7985 Mon Sep 17 00:00:00 2001 From: Talin Date: Tue, 8 Jul 2025 11:16:04 -0700 Subject: [PATCH 1/2] Owned callbacks. --- crates/bevy_core_widgets/src/callback.rs | 103 ++++++++++++++++++++++- crates/bevy_core_widgets/src/lib.rs | 3 +- crates/bevy_core_widgets/src/owner.rs | 51 +++++++++++ 3 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 crates/bevy_core_widgets/src/owner.rs diff --git a/crates/bevy_core_widgets/src/callback.rs b/crates/bevy_core_widgets/src/callback.rs index 37905e221cfcf..f6dcc5ae5ee9b 100644 --- a/crates/bevy_core_widgets/src/callback.rs +++ b/crates/bevy_core_widgets/src/callback.rs @@ -1,5 +1,7 @@ -use bevy_ecs::system::{Commands, SystemId, SystemInput}; -use bevy_ecs::world::{DeferredWorld, World}; +use bevy_ecs::component::Component; +use bevy_ecs::lifecycle::HookContext; +use bevy_ecs::system::{Commands, EntityCommands, IntoSystem, SystemId, SystemInput}; +use bevy_ecs::world::{DeferredWorld, EntityWorldMut, World}; /// A callback defines how we want to be notified when a widget changes state. Unlike an event /// or observer, callbacks are intended for "point-to-point" communication that cuts across the @@ -111,3 +113,100 @@ impl Notify for DeferredWorld<'_> { } } } + +/// A component that hangs on to a registered one-shot system, and unregisters it when the +/// component is despawned. +#[derive(Component)] +#[component(on_remove = on_despawn_callback_owner::, storage = "SparseSet")] +pub struct OwnedCallbackSystem(SystemId); + +fn on_despawn_callback_owner( + mut world: DeferredWorld, + context: HookContext, +) { + let system_id = world + .entity(context.entity) + .get::>() + .unwrap() + .0; + world.commands().unregister_system(system_id); +} + +/// Methods for registering scoped callbacks. +pub trait RegisterOwnedCallback { + /// Registers a scoped one-shot system, with no input, that will be removed when the parent + /// entity is despawned. + fn register_owned_callback + 'static>( + &mut self, + callback: I, + ) -> Callback; + + /// Registers a scoped one-shot systemm, with input, that will be removed when the + /// parent entity is despawned. + fn register_owned_callback_with< + M, + A: SystemInput + Send + 'static, + I: IntoSystem + 'static, + >( + &mut self, + callback: I, + ) -> Callback; +} + +impl RegisterOwnedCallback for EntityCommands<'_> { + fn register_owned_callback + 'static>( + &mut self, + callback: I, + ) -> Callback { + let system_id = self.commands().register_system(callback); + let owner = self.id(); + self.commands() + .spawn((OwnedCallbackSystem(system_id), crate::owner::OwnedBy(owner))); + Callback::System(system_id) + } + + fn register_owned_callback_with< + M, + A: SystemInput + Send + 'static, + I: IntoSystem + 'static, + >( + &mut self, + callback: I, + ) -> Callback { + let owner = self.id(); + let system_id = self.commands().register_system(callback); + self.commands() + .spawn((OwnedCallbackSystem(system_id), crate::owner::OwnedBy(owner))); + Callback::System(system_id) + } +} + +impl RegisterOwnedCallback for EntityWorldMut<'_> { + fn register_owned_callback + 'static>( + &mut self, + callback: I, + ) -> Callback { + let owner = self.id(); + let system_id = self.world_scope(|world| world.register_system(callback)); + self.world_scope(|world| { + world.spawn((OwnedCallbackSystem(system_id), crate::owner::OwnedBy(owner))); + }); + Callback::System(system_id) + } + + fn register_owned_callback_with< + M, + A: SystemInput + Send + 'static, + I: IntoSystem + 'static, + >( + &mut self, + callback: I, + ) -> Callback { + let owner = self.id(); + let system_id = self.world_scope(|world| world.register_system(callback)); + self.world_scope(|world| { + world.spawn((OwnedCallbackSystem(system_id), crate::owner::OwnedBy(owner))); + }); + Callback::System(system_id) + } +} diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs index 9a20b59c13032..82636e22db1b8 100644 --- a/crates/bevy_core_widgets/src/lib.rs +++ b/crates/bevy_core_widgets/src/lib.rs @@ -20,11 +20,12 @@ mod core_checkbox; mod core_radio; mod core_scrollbar; mod core_slider; +pub mod owner; use bevy_app::{PluginGroup, PluginGroupBuilder}; use bevy_ecs::entity::Entity; -pub use callback::{Callback, Notify}; +pub use callback::{Callback, Notify, RegisterOwnedCallback}; pub use core_button::{CoreButton, CoreButtonPlugin}; pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked}; pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin}; diff --git a/crates/bevy_core_widgets/src/owner.rs b/crates/bevy_core_widgets/src/owner.rs new file mode 100644 index 0000000000000..d295b1f40d2f9 --- /dev/null +++ b/crates/bevy_core_widgets/src/owner.rs @@ -0,0 +1,51 @@ +//! Defines relationships for ownership of an entity, with no other inherited semantics. +use core::slice; + +use bevy_ecs::{component::Component, entity::Entity}; + +/// A component that represents the owner of an entity. Ownership only determines lifetime, +/// such that the owned entity will be despawned when its owner is despawned. It does not imply +/// any other kind of semantic connection between the two entities. +// TODO: Consider renaming and/or moving this. +#[derive(Component, Clone, PartialEq, Eq, Debug)] +#[relationship(relationship_target = Owned)] +pub struct OwnedBy(pub Entity); + +impl OwnedBy { + /// Return the owned entity. + pub fn get(&self) -> Entity { + self.0 + } +} + +impl Default for OwnedBy { + fn default() -> Self { + OwnedBy(Entity::PLACEHOLDER) + } +} + +/// A component that represents a collection of entities that are owned by another entity. +// #[derive(Component, Default, Reflect)] +// #[reflect(Component)] +#[derive(Component, Default)] +#[relationship_target(relationship = OwnedBy, linked_spawn)] +pub struct Owned(Vec); + +impl<'a> IntoIterator for &'a Owned { + type Item = ::Item; + + type IntoIter = slice::Iter<'a, Entity>; + + #[inline(always)] + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl core::ops::Deref for Owned { + type Target = [Entity]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} From 7d1d8d7192d69b11e05503a0112fce42b5cb95af Mon Sep 17 00:00:00 2001 From: Talin Date: Wed, 16 Jul 2025 09:00:32 -0700 Subject: [PATCH 2/2] Simplified owned callbacks. --- crates/bevy_core_widgets/src/callback.rs | 36 ++++++++---------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/crates/bevy_core_widgets/src/callback.rs b/crates/bevy_core_widgets/src/callback.rs index f6dcc5ae5ee9b..64606ac0c46ed 100644 --- a/crates/bevy_core_widgets/src/callback.rs +++ b/crates/bevy_core_widgets/src/callback.rs @@ -1,8 +1,8 @@ -use bevy_ecs::component::Component; -use bevy_ecs::lifecycle::HookContext; use bevy_ecs::system::{Commands, EntityCommands, IntoSystem, SystemId, SystemInput}; use bevy_ecs::world::{DeferredWorld, EntityWorldMut, World}; +use crate::owner::OwnedBy; + /// A callback defines how we want to be notified when a widget changes state. Unlike an event /// or observer, callbacks are intended for "point-to-point" communication that cuts across the /// hierarchy of entities. Callbacks can be created in advance of the entity they are attached @@ -114,24 +114,6 @@ impl Notify for DeferredWorld<'_> { } } -/// A component that hangs on to a registered one-shot system, and unregisters it when the -/// component is despawned. -#[derive(Component)] -#[component(on_remove = on_despawn_callback_owner::, storage = "SparseSet")] -pub struct OwnedCallbackSystem(SystemId); - -fn on_despawn_callback_owner( - mut world: DeferredWorld, - context: HookContext, -) { - let system_id = world - .entity(context.entity) - .get::>() - .unwrap() - .0; - world.commands().unregister_system(system_id); -} - /// Methods for registering scoped callbacks. pub trait RegisterOwnedCallback { /// Registers a scoped one-shot system, with no input, that will be removed when the parent @@ -161,7 +143,8 @@ impl RegisterOwnedCallback for EntityCommands<'_> { let system_id = self.commands().register_system(callback); let owner = self.id(); self.commands() - .spawn((OwnedCallbackSystem(system_id), crate::owner::OwnedBy(owner))); + .entity(owner) + .add_one_related::(system_id.entity()); Callback::System(system_id) } @@ -176,7 +159,8 @@ impl RegisterOwnedCallback for EntityCommands<'_> { let owner = self.id(); let system_id = self.commands().register_system(callback); self.commands() - .spawn((OwnedCallbackSystem(system_id), crate::owner::OwnedBy(owner))); + .entity(owner) + .add_one_related::(system_id.entity()); Callback::System(system_id) } } @@ -189,7 +173,9 @@ impl RegisterOwnedCallback for EntityWorldMut<'_> { let owner = self.id(); let system_id = self.world_scope(|world| world.register_system(callback)); self.world_scope(|world| { - world.spawn((OwnedCallbackSystem(system_id), crate::owner::OwnedBy(owner))); + world + .entity_mut(owner) + .add_one_related::(system_id.entity()); }); Callback::System(system_id) } @@ -205,7 +191,9 @@ impl RegisterOwnedCallback for EntityWorldMut<'_> { let owner = self.id(); let system_id = self.world_scope(|world| world.register_system(callback)); self.world_scope(|world| { - world.spawn((OwnedCallbackSystem(system_id), crate::owner::OwnedBy(owner))); + world + .entity_mut(owner) + .add_one_related::(system_id.entity()); }); Callback::System(system_id) }