From 2055f68be876976047493e195edae3e49a64821c Mon Sep 17 00:00:00 2001 From: izarma Date: Sun, 17 May 2026 21:28:34 +0530 Subject: [PATCH 1/9] enabledin-disabledin --- crates/bevy_state/src/app.rs | 6 + crates/bevy_state/src/lib.rs | 2 +- crates/bevy_state/src/state_scoped.rs | 237 +++++++++++++++++++++++++- 3 files changed, 242 insertions(+), 3 deletions(-) diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index 8e6e56563077f..b4cbf939d2256 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -14,6 +14,8 @@ use crate::{ despawn_entities_when_state, disable_entities_on_enter_state, disable_entities_on_exit_state, disable_entities_when_state, enable_entities_on_enter_state, enable_entities_on_exit_state, enable_entities_when_state, + on_disabled_in_spawn, on_enabled_in_spawn, update_disabled_in_state, + update_enabled_in_state, }, }; @@ -280,9 +282,13 @@ fn enable_state_scoped_entities(app: &mut SubApp) { despawn_entities_when_state::, disable_entities_when_state::, enable_entities_when_state::, + update_enabled_in_state::, + update_disabled_in_state::, ) .in_set(StateTransitionSystems::TransitionSchedules), ); + app.add_observer(on_enabled_in_spawn::) + .add_observer(on_disabled_in_spawn::); } impl AppExtStates for App { diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs index 8059db544c107..93ffbe42bf890 100644 --- a/crates/bevy_state/src/lib.rs +++ b/crates/bevy_state/src/lib.rs @@ -94,7 +94,7 @@ pub mod prelude { }, state_scoped::{ DespawnOnEnter, DespawnOnExit, DespawnWhen, DisableOnEnter, DisableOnExit, DisableWhen, - EnableOnEnter, EnableOnExit, EnableWhen, + DisabledIn, EnableOnEnter, EnableOnExit, EnableWhen, EnabledIn, }, }; } diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index 8a7a01033ac2d..7f2b9a91e54eb 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -7,14 +7,16 @@ use bevy_ecs::{ entity::Entity, entity_disabling::Disabled, hierarchy::Children, + lifecycle::Insert, message::MessageReader, + observer::On, query::{Allow, With}, - system::{Commands, Query}, + system::{Commands, Query, Res}, }; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; -use crate::state::{StateTransitionEvent, States}; +use crate::state::{State, StateTransitionEvent, States}; /// Entities marked with this component will be despawned /// when a [`StateTransitionEvent`] matching the given predicate is sent. @@ -757,6 +759,201 @@ pub fn enable_entities_on_enter_state( } } +/// Entities marked with this component will be automatically Enabled when the world is in the given state, and disabled otherwise. +/// This component takes ownership of adding or removing entity's [`Disabled`] component. +/// Use [`EnableOnEnter`] and [`DisableOnExit`] separately if you need finer control. +/// +/// ``` +/// # use bevy_app::Startup; +/// use bevy_state::prelude::*; +/// use bevy_ecs::{prelude::*, system::ScheduleSystem}; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// InGame, +/// } +/// # #[derive(Component)] +/// # struct Player; +/// fn spawn_player(mut commands: Commands) { +/// commands.spawn(( +/// EnabledIn(GameState::InGame), +/// Player, +/// )); +/// } +/// +/// # struct AppMock; +/// # impl AppMock { +/// # fn init_state(&mut self) {} +/// # fn add_systems(&mut self, schedule: S, systems: impl IntoScheduleConfigs) {} +/// # } +/// # struct Update; +/// # let mut app = AppMock; +/// +/// app.init_state::(); +/// app.add_systems(Startup, spawn_player); +/// ``` +#[derive(Component, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] +pub struct EnabledIn(pub S); + +impl Default for EnabledIn { + fn default() -> Self { + Self(S::default()) + } +} + +/// Enables entities marked with [`EnabledIn`] when their state +/// matches the world state and disables when they don't. +pub fn update_enabled_in_state( + mut commands: Commands, + mut transitions: MessageReader>, + query: Query<(Entity, &EnabledIn), Allow>, +) { + // We use the latest event, because state machine internals generate at most 1 + // transition event (per type) each frame. No event means no change happened + // and we skip iterating all entities. + let Some(transition) = transitions.read().last() else { + return; + }; + if transition.entered == transition.exited && !transition.allow_same_state_transitions { + return; + } + for (entity, enabled_in) in &query { + if transition.entered.as_ref() == Some(&enabled_in.0) { + commands + .entity(entity) + .remove_recursive::(); + } else if transition.exited.as_ref() == Some(&enabled_in.0) { + commands + .entity(entity) + .insert_recursive::(Disabled); + } + } +} + +/// On [`EnabledIn`] insertion updates [`Disabled`] component depending on world state +pub fn on_enabled_in_spawn( + on: On>, + mut commands: Commands, + current_state: Option>>, + query: Query<&EnabledIn, Allow>, +) { + let entity = on.entity; + let Ok(enabled_in) = query.get(entity) else { + return; + }; + let in_target_state = current_state.is_some_and(|s| *s == enabled_in.0); + if in_target_state { + commands + .entity(entity) + .remove_recursive::(); + } else { + commands + .entity(entity) + .insert_recursive::(Disabled); + } +} + +/// Entities marked with this component will be automatically Disabled when the world is in the given state, and enabled otherwise. +/// This component takes ownership of adding or removing entity's [`Disabled`] component. +/// Use [`EnableOnEnter`] and [`DisableOnExit`] separately if you need finer control. +/// +/// ``` +/// # use bevy_app::Startup; +/// use bevy_state::prelude::*; +/// use bevy_ecs::{prelude::*, system::ScheduleSystem}; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// InGame, +/// } +/// # #[derive(Component)] +/// # struct Player; +/// fn spawn_player(mut commands: Commands) { +/// commands.spawn(( +/// DisabledIn(GameState::MainMenu), +/// Player, +/// )); +/// } +/// +/// # struct AppMock; +/// # impl AppMock { +/// # fn init_state(&mut self) {} +/// # fn add_systems(&mut self, schedule: S, systems: impl IntoScheduleConfigs) {} +/// # } +/// # struct Update; +/// # let mut app = AppMock; +/// +/// app.init_state::(); +/// app.add_systems(Startup, spawn_player); +/// ``` +#[derive(Component, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] +pub struct DisabledIn(pub S); + +impl Default for DisabledIn { + fn default() -> Self { + Self(S::default()) + } +} + +/// Disables entities marked with [`DisabledIn`] when their state +/// matches the world state and enables when they don't. +pub fn update_disabled_in_state( + mut commands: Commands, + mut transitions: MessageReader>, + query: Query<(Entity, &DisabledIn), Allow>, +) { + // We use the latest event, because state machine internals generate at most 1 + // transition event (per type) each frame. No event means no change happened + // and we skip iterating all entities. + let Some(transition) = transitions.read().last() else { + return; + }; + if transition.entered == transition.exited && !transition.allow_same_state_transitions { + return; + } + for (entity, disabled_in) in &query { + if transition.entered.as_ref() == Some(&disabled_in.0) { + commands + .entity(entity) + .insert_recursive::(Disabled); + } else if transition.exited.as_ref() == Some(&disabled_in.0) { + commands + .entity(entity) + .remove_recursive::(); + } + } +} + +/// On [`DisabledIn`] insertion updates [`Disabled`] component depending on world state +pub fn on_disabled_in_spawn( + on: On>, + mut commands: Commands, + current_state: Option>>, + query: Query<&DisabledIn, Allow>, +) { + let entity = on.entity; + let Ok(disabled_in) = query.get(entity) else { + return; + }; + // If state isn't initialized, assume it's not in the target state + let in_target_state = current_state.is_some_and(|s| *s == disabled_in.0); + if in_target_state { + commands + .entity(entity) + .insert_recursive::(Disabled); + } else { + commands + .entity(entity) + .remove_recursive::(); + } +} + #[cfg(test)] mod tests { use super::*; @@ -882,4 +1079,40 @@ mod tests { // the app's next state is the same as its previous. assert!(app.world().get_entity(entity).is_ok()); } + + #[test] + fn enabled_in_spawns_outside_target_state() { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)] + enum State { + On, + Off, + } + let mut app = App::new(); + app.add_plugins(StatesPlugin); + app.insert_state(State::Off); + app.update(); + let entity = app.world_mut().spawn(EnabledIn(State::On)).id(); + assert!(app.world().get::(entity).is_some()); + app.world_mut().commands().set_state(State::On); + app.update(); + assert!(app.world().get::(entity).is_none()); + } + + #[test] + fn disabled_in_spawns_outside_target_state() { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)] + enum State { + On, + Off, + } + let mut app = App::new(); + app.add_plugins(StatesPlugin); + app.insert_state(State::On); + app.update(); + let entity = app.world_mut().spawn(DisabledIn(State::Off)).id(); + assert!(app.world().get::(entity).is_none()); + app.world_mut().commands().set_state(State::Off); + app.update(); + assert!(app.world().get::(entity).is_some()); + } } From 73e928afac6ea433df1a1066e3deffcf2d1cd625 Mon Sep 17 00:00:00 2001 From: izarma Date: Tue, 19 May 2026 14:31:51 +0530 Subject: [PATCH 2/9] enabled-disabled-if --- crates/bevy_state/src/app.rs | 11 +- crates/bevy_state/src/lib.rs | 2 +- crates/bevy_state/src/state_scoped.rs | 343 ++++++++++++++++++++++++-- 3 files changed, 333 insertions(+), 23 deletions(-) diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index b4cbf939d2256..4ac8f98eb1b24 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -14,7 +14,8 @@ use crate::{ despawn_entities_when_state, disable_entities_on_enter_state, disable_entities_on_exit_state, disable_entities_when_state, enable_entities_on_enter_state, enable_entities_on_exit_state, enable_entities_when_state, - on_disabled_in_spawn, on_enabled_in_spawn, update_disabled_in_state, + on_disabled_if_insert, on_disabled_in_insert, on_enabled_if_insert, on_enabled_in_insert, + update_disabled_if_state, update_disabled_in_state, update_enabled_if_state, update_enabled_in_state, }, }; @@ -284,11 +285,15 @@ fn enable_state_scoped_entities(app: &mut SubApp) { enable_entities_when_state::, update_enabled_in_state::, update_disabled_in_state::, + update_enabled_if_state::, + update_disabled_if_state::, ) .in_set(StateTransitionSystems::TransitionSchedules), ); - app.add_observer(on_enabled_in_spawn::) - .add_observer(on_disabled_in_spawn::); + app.add_observer(on_enabled_in_insert::) + .add_observer(on_disabled_in_insert::) + .add_observer(on_enabled_if_insert::) + .add_observer(on_disabled_if_insert::); } impl AppExtStates for App { diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs index 93ffbe42bf890..19a912b306603 100644 --- a/crates/bevy_state/src/lib.rs +++ b/crates/bevy_state/src/lib.rs @@ -94,7 +94,7 @@ pub mod prelude { }, state_scoped::{ DespawnOnEnter, DespawnOnExit, DespawnWhen, DisableOnEnter, DisableOnExit, DisableWhen, - DisabledIn, EnableOnEnter, EnableOnExit, EnableWhen, EnabledIn, + DisabledIf, DisabledIn, EnableOnEnter, EnableOnExit, EnableWhen, EnabledIf, EnabledIn, }, }; } diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index 7f2b9a91e54eb..23c38dd2b7b38 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -759,8 +759,11 @@ pub fn enable_entities_on_enter_state( } } -/// Entities marked with this component will be automatically Enabled when the world is in the given state, and disabled otherwise. -/// This component takes ownership of adding or removing entity's [`Disabled`] component. +/// Entities marked with this component will be automatically enabled +/// when the world is in the given state, and disabled otherwise. +/// This component takes ownership of adding or removing entity's [`Disabled`] component +/// at state transitions and component insertion. +/// /// Use [`EnableOnEnter`] and [`DisableOnExit`] separately if you need finer control. /// /// ``` @@ -794,6 +797,8 @@ pub fn enable_entities_on_enter_state( /// app.init_state::(); /// app.add_systems(Startup, spawn_player); /// ``` +/// +/// See also [`EnabledIf`] and [`DisabledIn`]. #[derive(Component, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] pub struct EnabledIn(pub S); @@ -804,8 +809,8 @@ impl Default for EnabledIn { } } -/// Enables entities marked with [`EnabledIn`] when their state -/// matches the world state and disables when they don't. +/// Enables or disables entities marked with [`EnabledIn`] on state transition. +/// Removes [`Disabled`] when entering the target state, inserts it when exiting. pub fn update_enabled_in_state( mut commands: Commands, mut transitions: MessageReader>, @@ -833,8 +838,8 @@ pub fn update_enabled_in_state( } } -/// On [`EnabledIn`] insertion updates [`Disabled`] component depending on world state -pub fn on_enabled_in_spawn( +/// On [`EnabledIn`] insertion updates [`Disabled`] component of that entity based on current state. +pub fn on_enabled_in_insert( on: On>, mut commands: Commands, current_state: Option>>, @@ -844,7 +849,7 @@ pub fn on_enabled_in_spawn( let Ok(enabled_in) = query.get(entity) else { return; }; - let in_target_state = current_state.is_some_and(|s| *s == enabled_in.0); + let in_target_state = current_state.is_some_and(|s| s.get() == &enabled_in.0); if in_target_state { commands .entity(entity) @@ -856,9 +861,12 @@ pub fn on_enabled_in_spawn( } } -/// Entities marked with this component will be automatically Disabled when the world is in the given state, and enabled otherwise. -/// This component takes ownership of adding or removing entity's [`Disabled`] component. -/// Use [`EnableOnEnter`] and [`DisableOnExit`] separately if you need finer control. +/// Entities marked with this component will be automatically disabled +/// when the world is in the given state, and enabled otherwise. +/// This component takes ownership of adding or removing entity's [`Disabled`] component +/// at state transitions and component insertion. +/// +/// Use [`DisableOnEnter`] and [`EnableOnExit`] separately if you need finer control. /// /// ``` /// # use bevy_app::Startup; @@ -891,6 +899,8 @@ pub fn on_enabled_in_spawn( /// app.init_state::(); /// app.add_systems(Startup, spawn_player); /// ``` +/// +/// See also [`DisabledIf`] and [`EnabledIn`]. #[derive(Component, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] pub struct DisabledIn(pub S); @@ -901,8 +911,8 @@ impl Default for DisabledIn { } } -/// Disables entities marked with [`DisabledIn`] when their state -/// matches the world state and enables when they don't. +/// Disables or enables entities marked with [`DisabledIn`] on state transition. +/// Inserts [`Disabled`] when entering the target state, removes it when exiting. pub fn update_disabled_in_state( mut commands: Commands, mut transitions: MessageReader>, @@ -930,8 +940,8 @@ pub fn update_disabled_in_state( } } -/// On [`DisabledIn`] insertion updates [`Disabled`] component depending on world state -pub fn on_disabled_in_spawn( +/// On [`DisabledIn`] insertion updates [`Disabled`] component of that entity based on current state. +pub fn on_disabled_in_insert( on: On>, mut commands: Commands, current_state: Option>>, @@ -941,8 +951,7 @@ pub fn on_disabled_in_spawn( let Ok(disabled_in) = query.get(entity) else { return; }; - // If state isn't initialized, assume it's not in the target state - let in_target_state = current_state.is_some_and(|s| *s == disabled_in.0); + let in_target_state = current_state.is_some_and(|s| s.get() == &disabled_in.0); if in_target_state { commands .entity(entity) @@ -954,6 +963,233 @@ pub fn on_disabled_in_spawn( } } +/// Entities marked with this component will be automatically enabled +/// when predicate returns `true` for current state, disabled otherwise. +/// This component takes ownership of adding or removing entity's [`Disabled`] component +/// at state transitions and component insertion. +/// +/// ``` +/// # use bevy_app::Startup; +/// use bevy_state::prelude::*; +/// use bevy_ecs::{prelude::*, system::ScheduleSystem}; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// InGame, +/// } +/// # #[derive(Component)] +/// # struct Player; +/// fn spawn_player(mut commands: Commands) { +/// commands.spawn(( +/// EnabledIf::new(|s| matches!( +/// s, +/// GameState::InGame +/// )), +/// Player, +/// )); +/// } +/// +/// # struct AppMock; +/// # impl AppMock { +/// # fn init_state(&mut self) {} +/// # fn add_systems(&mut self, schedule: S, systems: impl IntoScheduleConfigs) {} +/// # } +/// # struct Update; +/// # let mut app = AppMock; +/// +/// app.init_state::(); +/// app.add_systems(Startup, spawn_player); +/// ``` +/// +/// See also [`DisabledIf`] and [`EnabledIn`]. +#[derive(Component)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] +pub struct EnabledIf { + /// The predicate used to determine if the entity should be enabled for the given state. + pub predicate: Box bool + Sync + Send + 'static>, +} + +impl EnabledIf { + /// Creates an [`EnabledIf`] for the given predicate. + pub fn new(f: impl Fn(&S) -> bool + Sync + Send + 'static) -> Self { + Self { + predicate: Box::new(f), + } + } +} + +/// Enables or disables entities marked with [`EnabledIf`] based on their predicate evaluated against the entered state. +/// Removes [`Disabled`] when `true` and inserts it when `false`. +pub fn update_enabled_if_state( + mut commands: Commands, + mut transitions: MessageReader>, + query: Query<(Entity, &EnabledIf), Allow>, +) { + // We use the latest event, because state machine internals generate at most 1 + // transition event (per type) each frame. No event means no change happened + // and we skip iterating all entities. + let Some(transition) = transitions.read().last() else { + return; + }; + if transition.entered == transition.exited && !transition.allow_same_state_transitions { + return; + } + for (entity, enabled_if) in &query { + let should_enable = transition + .entered + .as_ref() + .is_some_and(|s| (enabled_if.predicate)(s)); + if should_enable { + commands + .entity(entity) + .remove_recursive::(); + } else { + commands + .entity(entity) + .insert_recursive::(Disabled); + } + } +} + +/// On [`EnabledIf`] insertion updates [`Disabled`] component depending on predicate evaluated against the current state. +pub fn on_enabled_if_insert( + on: On>, + mut commands: Commands, + current_state: Option>>, + query: Query<&EnabledIf, Allow>, +) { + let entity = on.entity; + let Ok(enabled_if) = query.get(entity) else { + return; + }; + let should_enable = current_state.is_some_and(|s| (enabled_if.predicate)(s.get())); + if should_enable { + commands + .entity(entity) + .remove_recursive::(); + } else { + commands + .entity(entity) + .insert_recursive::(Disabled); + } +} + +/// Entities marked with this component will be automatically disabled +/// when predicate returns `true` for current state, enabled otherwise. +/// This component takes ownership of adding or removing entity's [`Disabled`] component +/// at state transitions and component insertion. +/// +/// ``` +/// # use bevy_app::Startup; +/// use bevy_state::prelude::*; +/// use bevy_ecs::{prelude::*, system::ScheduleSystem}; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// InGame, +/// Settings, +/// } +/// # #[derive(Component)] +/// # struct Player; +/// fn spawn_player(mut commands: Commands) { +/// commands.spawn(( +/// DisabledIf::new(|s| matches!( +/// s, +/// GameState::MainMenu | GameState::Settings +/// )), +/// Player, +/// )); +/// } +/// +/// # struct AppMock; +/// # impl AppMock { +/// # fn init_state(&mut self) {} +/// # fn add_systems(&mut self, schedule: S, systems: impl IntoScheduleConfigs) {} +/// # } +/// # struct Update; +/// # let mut app = AppMock; +/// +/// app.init_state::(); +/// app.add_systems(Startup, spawn_player); +/// ``` +/// +/// See also [`EnabledIf`] and [`DisabledIn`]. +#[derive(Component)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] +pub struct DisabledIf { + /// The predicate used to determine if the entity should be disabled for the given state. + pub predicate: Box bool + Sync + Send + 'static>, +} + +impl DisabledIf { + /// Creates a [`DisabledIf`] for the given predicate. + pub fn new(f: impl Fn(&S) -> bool + Sync + Send + 'static) -> Self { + Self { + predicate: Box::new(f), + } + } +} + +/// Disables or enables entities marked with [`DisabledIf`] based on their predicate evaluated against the entered state. +/// Inserts [`Disabled`] when `true` and removes it when `false`. +pub fn update_disabled_if_state( + mut commands: Commands, + mut transitions: MessageReader>, + query: Query<(Entity, &DisabledIf), Allow>, +) { + // We use the latest event, because state machine internals generate at most 1 + // transition event (per type) each frame. No event means no change happened + // and we skip iterating all entities. + let Some(transition) = transitions.read().last() else { + return; + }; + if transition.entered == transition.exited && !transition.allow_same_state_transitions { + return; + } + for (entity, disabled_if) in &query { + let should_disable = transition + .entered + .as_ref() + .is_some_and(|s| (disabled_if.predicate)(s)); + if should_disable { + commands + .entity(entity) + .insert_recursive::(Disabled); + } else { + commands + .entity(entity) + .remove_recursive::(); + } + } +} + +/// On [`DisabledIf`] insertion updates [`Disabled`] component depending on predicate evaluated against the current state. +pub fn on_disabled_if_insert( + on: On>, + mut commands: Commands, + current_state: Option>>, + query: Query<&DisabledIf, Allow>, +) { + let entity = on.entity; + let Ok(disabled_if) = query.get(entity) else { + return; + }; + let should_disable = current_state.is_some_and(|s| (disabled_if.predicate)(s.get())); + if should_disable { + commands + .entity(entity) + .insert_recursive::(Disabled); + } else { + commands + .entity(entity) + .remove_recursive::(); + } +} + #[cfg(test)] mod tests { use super::*; @@ -1089,17 +1325,19 @@ mod tests { } let mut app = App::new(); app.add_plugins(StatesPlugin); + app.insert_state(State::Off); app.update(); let entity = app.world_mut().spawn(EnabledIn(State::On)).id(); assert!(app.world().get::(entity).is_some()); + app.world_mut().commands().set_state(State::On); app.update(); assert!(app.world().get::(entity).is_none()); } #[test] - fn disabled_in_spawns_outside_target_state() { + fn disabled_in_spawns_inside_target_state() { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)] enum State { On, @@ -1107,11 +1345,78 @@ mod tests { } let mut app = App::new(); app.add_plugins(StatesPlugin); - app.insert_state(State::On); + + app.insert_state(State::Off); app.update(); let entity = app.world_mut().spawn(DisabledIn(State::Off)).id(); + assert!(app.world().get::(entity).is_some()); + + app.world_mut().commands().set_state(State::On); + app.update(); assert!(app.world().get::(entity).is_none()); - app.world_mut().commands().set_state(State::Off); + } + + #[test] + fn enabled_if_spawns_outside_target_state() { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)] + enum State { + On, + Off, + Limbo, + } + + let mut app = App::new(); + app.add_plugins(StatesPlugin); + + app.insert_state(State::Off); + app.update(); + let entity = app + .world_mut() + .spawn(EnabledIf::new(|state: &State| { + matches!(state, State::On | State::Limbo) // predicate evals to false - Disabled + })) + .id(); + assert!(app.world().get::(entity).is_some()); + + // predicate evals to true - Enabled + app.world_mut().commands().set_state(State::Limbo); + app.update(); + assert!(app.world().get::(entity).is_none()); + + // predicate evals to true - Enabled + app.world_mut().commands().set_state(State::On); + app.update(); + assert!(app.world().get::(entity).is_none()); + } + + #[test] + fn disabled_if_spawns_inside_target_state() { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)] + enum State { + On, + Off, + Limbo, + } + let mut app = App::new(); + app.add_plugins(StatesPlugin); + + app.insert_state(State::Off); + app.update(); + let entity = app + .world_mut() + .spawn(DisabledIf::new(|state: &State| { + matches!(state, State::Off | State::Limbo) // predicate evals to true - Disabled + })) + .id(); + assert!(app.world().get::(entity).is_some()); + + // predicate evals to false - Enabled + app.world_mut().commands().set_state(State::On); + app.update(); + assert!(app.world().get::(entity).is_none()); + + // predicate evals to true - Disabled + app.world_mut().commands().set_state(State::Limbo); app.update(); assert!(app.world().get::(entity).is_some()); } From 435291aeb3284e179593dda816eeb775cd5ba498 Mon Sep 17 00:00:00 2001 From: izarma Date: Tue, 19 May 2026 19:48:19 +0530 Subject: [PATCH 3/9] on_remove observer --- crates/bevy_state/src/app.rs | 11 +++++-- crates/bevy_state/src/state_scoped.rs | 45 ++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index 4ac8f98eb1b24..b9f02b38c53a1 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -15,8 +15,9 @@ use crate::{ disable_entities_on_exit_state, disable_entities_when_state, enable_entities_on_enter_state, enable_entities_on_exit_state, enable_entities_when_state, on_disabled_if_insert, on_disabled_in_insert, on_enabled_if_insert, on_enabled_in_insert, - update_disabled_if_state, update_disabled_in_state, update_enabled_if_state, - update_enabled_in_state, + on_state_disabled_component_remove, update_disabled_if_state, update_disabled_in_state, + update_enabled_if_state, update_enabled_in_state, DisabledIf, DisabledIn, EnabledIf, + EnabledIn, }, }; @@ -293,7 +294,11 @@ fn enable_state_scoped_entities(app: &mut SubApp) { app.add_observer(on_enabled_in_insert::) .add_observer(on_disabled_in_insert::) .add_observer(on_enabled_if_insert::) - .add_observer(on_disabled_if_insert::); + .add_observer(on_disabled_if_insert::) + .add_observer(on_state_disabled_component_remove::>) + .add_observer(on_state_disabled_component_remove::>) + .add_observer(on_state_disabled_component_remove::>) + .add_observer(on_state_disabled_component_remove::>); } impl AppExtStates for App { diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index 23c38dd2b7b38..b33f501569661 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -7,7 +7,7 @@ use bevy_ecs::{ entity::Entity, entity_disabling::Disabled, hierarchy::Children, - lifecycle::Insert, + lifecycle::{Insert, Remove}, message::MessageReader, observer::On, query::{Allow, With}, @@ -764,6 +764,9 @@ pub fn enable_entities_on_enter_state( /// This component takes ownership of adding or removing entity's [`Disabled`] component /// at state transitions and component insertion. /// +/// # Safety +/// System is added on state registration, so `Res>` should always exist +/// /// Use [`EnableOnEnter`] and [`DisableOnExit`] separately if you need finer control. /// /// ``` @@ -842,14 +845,14 @@ pub fn update_enabled_in_state( pub fn on_enabled_in_insert( on: On>, mut commands: Commands, - current_state: Option>>, + current_state: Res>, query: Query<&EnabledIn, Allow>, ) { let entity = on.entity; let Ok(enabled_in) = query.get(entity) else { return; }; - let in_target_state = current_state.is_some_and(|s| s.get() == &enabled_in.0); + let in_target_state = current_state.get() == &enabled_in.0; if in_target_state { commands .entity(entity) @@ -866,6 +869,9 @@ pub fn on_enabled_in_insert( /// This component takes ownership of adding or removing entity's [`Disabled`] component /// at state transitions and component insertion. /// +/// # Safety +/// System is added on state registration, so `Res>` should always exist +/// /// Use [`DisableOnEnter`] and [`EnableOnExit`] separately if you need finer control. /// /// ``` @@ -944,14 +950,14 @@ pub fn update_disabled_in_state( pub fn on_disabled_in_insert( on: On>, mut commands: Commands, - current_state: Option>>, + current_state: Res>, query: Query<&DisabledIn, Allow>, ) { let entity = on.entity; let Ok(disabled_in) = query.get(entity) else { return; }; - let in_target_state = current_state.is_some_and(|s| s.get() == &disabled_in.0); + let in_target_state = current_state.get() == &disabled_in.0; if in_target_state { commands .entity(entity) @@ -968,6 +974,10 @@ pub fn on_disabled_in_insert( /// This component takes ownership of adding or removing entity's [`Disabled`] component /// at state transitions and component insertion. /// +/// # Safety +/// System is added on state registration, so `Res>` should always exist +/// +/// /// ``` /// # use bevy_app::Startup; /// use bevy_state::prelude::*; @@ -1081,6 +1091,9 @@ pub fn on_enabled_if_insert( /// This component takes ownership of adding or removing entity's [`Disabled`] component /// at state transitions and component insertion. /// +/// # Safety +/// System is added on state registration, so `Res>` should always exist +/// /// ``` /// # use bevy_app::Startup; /// use bevy_state::prelude::*; @@ -1171,14 +1184,14 @@ pub fn update_disabled_if_state( pub fn on_disabled_if_insert( on: On>, mut commands: Commands, - current_state: Option>>, + current_state: Res>, query: Query<&DisabledIf, Allow>, ) { let entity = on.entity; let Ok(disabled_if) = query.get(entity) else { return; }; - let should_disable = current_state.is_some_and(|s| (disabled_if.predicate)(s.get())); + let should_disable = (disabled_if.predicate)(current_state.get()); if should_disable { commands .entity(entity) @@ -1190,6 +1203,14 @@ pub fn on_disabled_if_insert( } } +/// Removes [`Disabled`] component from an entity when its managing state-driven disabling components are removed. +/// i.e. [`EnabledIf`], [`DisabledIf`], [`EnabledIn`], [`DisabledIn`] +pub fn on_state_disabled_component_remove(on: On, mut commands: Commands) { + commands + .entity(on.entity) + .remove_recursive::(); +} + #[cfg(test)] mod tests { use super::*; @@ -1334,6 +1355,16 @@ mod tests { app.world_mut().commands().set_state(State::On); app.update(); assert!(app.world().get::(entity).is_none()); + + app.world_mut().commands().set_state(State::Off); + app.update(); + assert!(app.world().get::(entity).is_some()); + + // cleanup observer should remove the disabled component + app.world_mut() + .entity_mut(entity) + .remove::>(); + assert!(app.world().get::(entity).is_none()); } #[test] From 1da4f5439d691834f8d02844a764755af1a0270c Mon Sep 17 00:00:00 2001 From: izarma Date: Fri, 12 Jun 2026 04:48:08 +0530 Subject: [PATCH 4/9] stateownsdisabled --- crates/bevy_state/src/app.rs | 11 +--- crates/bevy_state/src/state_scoped.rs | 87 +++++++++------------------ 2 files changed, 33 insertions(+), 65 deletions(-) diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index b9f02b38c53a1..4ac8f98eb1b24 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -15,9 +15,8 @@ use crate::{ disable_entities_on_exit_state, disable_entities_when_state, enable_entities_on_enter_state, enable_entities_on_exit_state, enable_entities_when_state, on_disabled_if_insert, on_disabled_in_insert, on_enabled_if_insert, on_enabled_in_insert, - on_state_disabled_component_remove, update_disabled_if_state, update_disabled_in_state, - update_enabled_if_state, update_enabled_in_state, DisabledIf, DisabledIn, EnabledIf, - EnabledIn, + update_disabled_if_state, update_disabled_in_state, update_enabled_if_state, + update_enabled_in_state, }, }; @@ -294,11 +293,7 @@ fn enable_state_scoped_entities(app: &mut SubApp) { app.add_observer(on_enabled_in_insert::) .add_observer(on_disabled_in_insert::) .add_observer(on_enabled_if_insert::) - .add_observer(on_disabled_if_insert::) - .add_observer(on_state_disabled_component_remove::>) - .add_observer(on_state_disabled_component_remove::>) - .add_observer(on_state_disabled_component_remove::>) - .add_observer(on_state_disabled_component_remove::>); + .add_observer(on_disabled_if_insert::); } impl AppExtStates for App { diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index b33f501569661..90ec79458e96e 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -1,3 +1,5 @@ +use std::vec::Vec; + use alloc::boxed::Box; #[cfg(feature = "bevy_reflect")] @@ -7,11 +9,12 @@ use bevy_ecs::{ entity::Entity, entity_disabling::Disabled, hierarchy::Children, - lifecycle::{Insert, Remove}, + lifecycle::Insert, message::MessageReader, observer::On, query::{Allow, With}, system::{Commands, Query, Res}, + world::World, }; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; @@ -759,6 +762,12 @@ pub fn enable_entities_on_enter_state( } } +/// Prevents parent state-driven [`Disabled`] propagation from affecting this entity +/// and its subtree. Added automatically by [`EnabledIn`], [`DisabledIn`], +/// [`EnabledIf`], and [`DisabledIf`]. Can also be added manually. +#[derive(Component, Clone, Default)] +pub struct StateOwnsDisabled; + /// Entities marked with this component will be automatically enabled /// when the world is in the given state, and disabled otherwise. /// This component takes ownership of adding or removing entity's [`Disabled`] component @@ -803,6 +812,7 @@ pub fn enable_entities_on_enter_state( /// /// See also [`EnabledIf`] and [`DisabledIn`]. #[derive(Component, Clone)] +#[require(StateOwnsDisabled)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] pub struct EnabledIn(pub S); @@ -830,13 +840,9 @@ pub fn update_enabled_in_state( } for (entity, enabled_in) in &query { if transition.entered.as_ref() == Some(&enabled_in.0) { - commands - .entity(entity) - .remove_recursive::(); + enable_recursive(&mut commands, entity); } else if transition.exited.as_ref() == Some(&enabled_in.0) { - commands - .entity(entity) - .insert_recursive::(Disabled); + disable_recursive(&mut commands, entity); } } } @@ -854,13 +860,9 @@ pub fn on_enabled_in_insert( }; let in_target_state = current_state.get() == &enabled_in.0; if in_target_state { - commands - .entity(entity) - .remove_recursive::(); + enable_recursive(&mut commands, entity); } else { - commands - .entity(entity) - .insert_recursive::(Disabled); + disable_recursive(&mut commands, entity); } } @@ -908,6 +910,7 @@ pub fn on_enabled_in_insert( /// /// See also [`DisabledIf`] and [`EnabledIn`]. #[derive(Component, Clone)] +#[require(StateOwnsDisabled)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] pub struct DisabledIn(pub S); @@ -935,13 +938,9 @@ pub fn update_disabled_in_state( } for (entity, disabled_in) in &query { if transition.entered.as_ref() == Some(&disabled_in.0) { - commands - .entity(entity) - .insert_recursive::(Disabled); + disable_recursive(&mut commands, entity); } else if transition.exited.as_ref() == Some(&disabled_in.0) { - commands - .entity(entity) - .remove_recursive::(); + enable_recursive(&mut commands, entity); } } } @@ -959,13 +958,9 @@ pub fn on_disabled_in_insert( }; let in_target_state = current_state.get() == &disabled_in.0; if in_target_state { - commands - .entity(entity) - .insert_recursive::(Disabled); + disable_recursive(&mut commands, entity); } else { - commands - .entity(entity) - .remove_recursive::(); + enable_recursive(&mut commands, entity); } } @@ -1015,6 +1010,7 @@ pub fn on_disabled_in_insert( /// /// See also [`DisabledIf`] and [`EnabledIn`]. #[derive(Component)] +#[require(StateOwnsDisabled)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] pub struct EnabledIf { /// The predicate used to determine if the entity should be enabled for the given state. @@ -1052,13 +1048,9 @@ pub fn update_enabled_if_state( .as_ref() .is_some_and(|s| (enabled_if.predicate)(s)); if should_enable { - commands - .entity(entity) - .remove_recursive::(); + enable_recursive(&mut commands, entity); } else { - commands - .entity(entity) - .insert_recursive::(Disabled); + disable_recursive(&mut commands, entity); } } } @@ -1076,13 +1068,9 @@ pub fn on_enabled_if_insert( }; let should_enable = current_state.is_some_and(|s| (enabled_if.predicate)(s.get())); if should_enable { - commands - .entity(entity) - .remove_recursive::(); + enable_recursive(&mut commands, entity); } else { - commands - .entity(entity) - .insert_recursive::(Disabled); + disable_recursive(&mut commands, entity); } } @@ -1132,6 +1120,7 @@ pub fn on_enabled_if_insert( /// /// See also [`EnabledIf`] and [`DisabledIn`]. #[derive(Component)] +#[require(StateOwnsDisabled)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] pub struct DisabledIf { /// The predicate used to determine if the entity should be disabled for the given state. @@ -1169,13 +1158,9 @@ pub fn update_disabled_if_state( .as_ref() .is_some_and(|s| (disabled_if.predicate)(s)); if should_disable { - commands - .entity(entity) - .insert_recursive::(Disabled); + disable_recursive(&mut commands, entity); } else { - commands - .entity(entity) - .remove_recursive::(); + enable_recursive(&mut commands, entity); } } } @@ -1193,24 +1178,12 @@ pub fn on_disabled_if_insert( }; let should_disable = (disabled_if.predicate)(current_state.get()); if should_disable { - commands - .entity(entity) - .insert_recursive::(Disabled); + disable_recursive(&mut commands, entity); } else { - commands - .entity(entity) - .remove_recursive::(); + enable_recursive(&mut commands, entity); } } -/// Removes [`Disabled`] component from an entity when its managing state-driven disabling components are removed. -/// i.e. [`EnabledIf`], [`DisabledIf`], [`EnabledIn`], [`DisabledIn`] -pub fn on_state_disabled_component_remove(on: On, mut commands: Commands) { - commands - .entity(on.entity) - .remove_recursive::(); -} - #[cfg(test)] mod tests { use super::*; From be0b975c0a3d3cd1cc694ec4eb59de8d543b6f23 Mon Sep 17 00:00:00 2001 From: izarma Date: Sun, 21 Jun 2026 20:38:27 +0530 Subject: [PATCH 5/9] propogate_enabled-disabled --- crates/bevy_state/src/app.rs | 11 +- crates/bevy_state/src/lib.rs | 1 + crates/bevy_state/src/state_scoped.rs | 272 ++++++++++++++++++++++---- 3 files changed, 239 insertions(+), 45 deletions(-) diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index 4ac8f98eb1b24..b9f02b38c53a1 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -15,8 +15,9 @@ use crate::{ disable_entities_on_exit_state, disable_entities_when_state, enable_entities_on_enter_state, enable_entities_on_exit_state, enable_entities_when_state, on_disabled_if_insert, on_disabled_in_insert, on_enabled_if_insert, on_enabled_in_insert, - update_disabled_if_state, update_disabled_in_state, update_enabled_if_state, - update_enabled_in_state, + on_state_disabled_component_remove, update_disabled_if_state, update_disabled_in_state, + update_enabled_if_state, update_enabled_in_state, DisabledIf, DisabledIn, EnabledIf, + EnabledIn, }, }; @@ -293,7 +294,11 @@ fn enable_state_scoped_entities(app: &mut SubApp) { app.add_observer(on_enabled_in_insert::) .add_observer(on_disabled_in_insert::) .add_observer(on_enabled_if_insert::) - .add_observer(on_disabled_if_insert::); + .add_observer(on_disabled_if_insert::) + .add_observer(on_state_disabled_component_remove::>) + .add_observer(on_state_disabled_component_remove::>) + .add_observer(on_state_disabled_component_remove::>) + .add_observer(on_state_disabled_component_remove::>); } impl AppExtStates for App { diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs index 19a912b306603..f0a329f86c5eb 100644 --- a/crates/bevy_state/src/lib.rs +++ b/crates/bevy_state/src/lib.rs @@ -95,6 +95,7 @@ pub mod prelude { state_scoped::{ DespawnOnEnter, DespawnOnExit, DespawnWhen, DisableOnEnter, DisableOnExit, DisableWhen, DisabledIf, DisabledIn, EnableOnEnter, EnableOnExit, EnableWhen, EnabledIf, EnabledIn, + OwnsDisabled, }, }; } diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index 90ec79458e96e..4ce7feb5a4dd6 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -1,6 +1,4 @@ -use std::vec::Vec; - -use alloc::boxed::Box; +use alloc::{boxed::Box, vec, vec::Vec}; #[cfg(feature = "bevy_reflect")] use bevy_ecs::reflect::ReflectComponent; @@ -9,7 +7,7 @@ use bevy_ecs::{ entity::Entity, entity_disabling::Disabled, hierarchy::Children, - lifecycle::Insert, + lifecycle::{Insert, Remove}, message::MessageReader, observer::On, query::{Allow, With}, @@ -762,22 +760,81 @@ pub fn enable_entities_on_enter_state( } } -/// Prevents parent state-driven [`Disabled`] propagation from affecting this entity -/// and its subtree. Added automatically by [`EnabledIn`], [`DisabledIn`], -/// [`EnabledIf`], and [`DisabledIf`]. Can also be added manually. +/// Marks this entity as owning its [`Disabled`] state. +/// When present, parent-driven enable/disable propogation skips this entity +/// and its descendants. +/// +/// Automatically required by state-driven disabling components +/// i.e. ([`EnabledIn`], [`DisabledIn`], [`EnabledIf`], [`DisabledIf`]). +/// Can be inserted manually to shield an entity from parent's disabled propagation. +/// ``` +/// # use bevy_app::Startup; +/// use bevy_state::prelude::*; +/// use bevy_ecs::{prelude::*, system::ScheduleSystem, entity_disabling::Disabled}; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// InGame, +/// } +/// +/// # #[derive(Component)] +/// # struct ParentEntity; +/// # #[derive(Component)] +/// # struct ShieldedChild; +/// fn spawn_parent_entity(mut commands: Commands) { +/// commands.spawn(( +/// ParentEntity, +/// EnabledIn(GameState::MainMenu), +/// children![( +/// // This entity and its descendants will be ignored by parent state transitions. +/// ShieldedChild, +/// OwnsDisabled, +/// Disabled, +/// )] +/// )); +/// } +/// +/// # struct AppMock; +/// # impl AppMock { +/// # fn init_state(&mut self) {} +/// # fn add_systems(&mut self, schedule: S, systems: impl IntoScheduleConfigs) {} +/// # } +/// # struct Update; +/// # let mut app = AppMock; +/// +/// app.init_state::(); +/// app.add_systems(Startup, spawn_parent_entity); +/// ``` +/// +/// See also [`EnabledIn`], [`DisabledIn`], [`EnabledIf`], and [`DisabledIf`]. #[derive(Component, Clone, Default)] -pub struct StateOwnsDisabled; +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Component, Clone, Default) +)] +pub struct OwnsDisabled; + +/// Removes [`OwnsDisabled`] component from an entity +/// when its managing state-driven disabling components are removed +/// i.e. ([`EnabledIn`], [`DisabledIn`], [`EnabledIf`], [`DisabledIf`]). +pub fn on_state_disabled_component_remove(on: On, mut commands: Commands) { + let mut entity = commands.entity(on.entity); + entity.remove::(); +} /// Entities marked with this component will be automatically enabled /// when the world is in the given state, and disabled otherwise. +/// /// This component takes ownership of adding or removing entity's [`Disabled`] component /// at state transitions and component insertion. +/// At component removal, [`Disabled`] is left as is. /// -/// # Safety +/// # Note /// System is added on state registration, so `Res>` should always exist /// -/// Use [`EnableOnEnter`] and [`DisableOnExit`] separately if you need finer control. -/// /// ``` /// # use bevy_app::Startup; /// use bevy_state::prelude::*; @@ -810,10 +867,11 @@ pub struct StateOwnsDisabled; /// app.add_systems(Startup, spawn_player); /// ``` /// +/// Use [`EnableOnEnter`] and [`DisableOnExit`] separately if you need finer control. /// See also [`EnabledIf`] and [`DisabledIn`]. #[derive(Component, Clone)] -#[require(StateOwnsDisabled)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] +#[require(OwnsDisabled)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))] pub struct EnabledIn(pub S); impl Default for EnabledIn { @@ -840,9 +898,9 @@ pub fn update_enabled_in_state( } for (entity, enabled_in) in &query { if transition.entered.as_ref() == Some(&enabled_in.0) { - enable_recursive(&mut commands, entity); + propagate_enable(&mut commands, entity); } else if transition.exited.as_ref() == Some(&enabled_in.0) { - disable_recursive(&mut commands, entity); + propagate_disable(&mut commands, entity); } } } @@ -860,22 +918,22 @@ pub fn on_enabled_in_insert( }; let in_target_state = current_state.get() == &enabled_in.0; if in_target_state { - enable_recursive(&mut commands, entity); + propagate_enable(&mut commands, entity); } else { - disable_recursive(&mut commands, entity); + propagate_disable(&mut commands, entity); } } /// Entities marked with this component will be automatically disabled /// when the world is in the given state, and enabled otherwise. +/// /// This component takes ownership of adding or removing entity's [`Disabled`] component /// at state transitions and component insertion. +/// At component removal, [`Disabled`] is left as is. /// -/// # Safety +/// # Note /// System is added on state registration, so `Res>` should always exist /// -/// Use [`DisableOnEnter`] and [`EnableOnExit`] separately if you need finer control. -/// /// ``` /// # use bevy_app::Startup; /// use bevy_state::prelude::*; @@ -908,10 +966,11 @@ pub fn on_enabled_in_insert( /// app.add_systems(Startup, spawn_player); /// ``` /// +/// Use [`DisableOnEnter`] and [`EnableOnExit`] separately if you need finer control. /// See also [`DisabledIf`] and [`EnabledIn`]. #[derive(Component, Clone)] -#[require(StateOwnsDisabled)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] +#[require(OwnsDisabled)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))] pub struct DisabledIn(pub S); impl Default for DisabledIn { @@ -938,9 +997,9 @@ pub fn update_disabled_in_state( } for (entity, disabled_in) in &query { if transition.entered.as_ref() == Some(&disabled_in.0) { - disable_recursive(&mut commands, entity); + propagate_disable(&mut commands, entity); } else if transition.exited.as_ref() == Some(&disabled_in.0) { - enable_recursive(&mut commands, entity); + propagate_enable(&mut commands, entity); } } } @@ -958,18 +1017,20 @@ pub fn on_disabled_in_insert( }; let in_target_state = current_state.get() == &disabled_in.0; if in_target_state { - disable_recursive(&mut commands, entity); + propagate_disable(&mut commands, entity); } else { - enable_recursive(&mut commands, entity); + propagate_enable(&mut commands, entity); } } /// Entities marked with this component will be automatically enabled /// when predicate returns `true` for current state, disabled otherwise. +/// /// This component takes ownership of adding or removing entity's [`Disabled`] component /// at state transitions and component insertion. +/// At component removal, [`Disabled`] is left as is. /// -/// # Safety +/// # Note /// System is added on state registration, so `Res>` should always exist /// /// @@ -1010,7 +1071,7 @@ pub fn on_disabled_in_insert( /// /// See also [`DisabledIf`] and [`EnabledIn`]. #[derive(Component)] -#[require(StateOwnsDisabled)] +#[require(OwnsDisabled)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] pub struct EnabledIf { /// The predicate used to determine if the entity should be enabled for the given state. @@ -1048,9 +1109,9 @@ pub fn update_enabled_if_state( .as_ref() .is_some_and(|s| (enabled_if.predicate)(s)); if should_enable { - enable_recursive(&mut commands, entity); + propagate_enable(&mut commands, entity); } else { - disable_recursive(&mut commands, entity); + propagate_disable(&mut commands, entity); } } } @@ -1059,27 +1120,29 @@ pub fn update_enabled_if_state( pub fn on_enabled_if_insert( on: On>, mut commands: Commands, - current_state: Option>>, + current_state: Res>, query: Query<&EnabledIf, Allow>, ) { let entity = on.entity; let Ok(enabled_if) = query.get(entity) else { return; }; - let should_enable = current_state.is_some_and(|s| (enabled_if.predicate)(s.get())); + let should_enable = (enabled_if.predicate)(current_state.get()); if should_enable { - enable_recursive(&mut commands, entity); + propagate_enable(&mut commands, entity); } else { - disable_recursive(&mut commands, entity); + propagate_disable(&mut commands, entity); } } /// Entities marked with this component will be automatically disabled /// when predicate returns `true` for current state, enabled otherwise. +/// /// This component takes ownership of adding or removing entity's [`Disabled`] component /// at state transitions and component insertion. +/// At component removal, [`Disabled`] is left as is. /// -/// # Safety +/// # Note /// System is added on state registration, so `Res>` should always exist /// /// ``` @@ -1120,7 +1183,7 @@ pub fn on_enabled_if_insert( /// /// See also [`EnabledIf`] and [`DisabledIn`]. #[derive(Component)] -#[require(StateOwnsDisabled)] +#[require(OwnsDisabled)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] pub struct DisabledIf { /// The predicate used to determine if the entity should be disabled for the given state. @@ -1158,9 +1221,9 @@ pub fn update_disabled_if_state( .as_ref() .is_some_and(|s| (disabled_if.predicate)(s)); if should_disable { - disable_recursive(&mut commands, entity); + propagate_disable(&mut commands, entity); } else { - enable_recursive(&mut commands, entity); + propagate_enable(&mut commands, entity); } } } @@ -1178,12 +1241,58 @@ pub fn on_disabled_if_insert( }; let should_disable = (disabled_if.predicate)(current_state.get()); if should_disable { - disable_recursive(&mut commands, entity); + propagate_disable(&mut commands, entity); } else { - enable_recursive(&mut commands, entity); + propagate_enable(&mut commands, entity); } } +/// Propagates enabling to `entity` and its descendants, stopping at [`OwnsDisabled`]. +fn propagate_enable(commands: &mut Commands, entity: Entity) { + commands.queue(move |world: &mut World| { + let mut stack = vec![entity]; + while let Some(current) = stack.pop() { + let Ok(mut entity_mut) = world.get_entity_mut(current) else { + continue; + }; + entity_mut.remove::(); + let children: Vec = entity_mut + .get::() + .map(|c| c.iter().copied().collect()) + .unwrap_or_default(); + drop(entity_mut); + for child in children { + if world.get::(child).is_none() { + stack.push(child); + } + } + } + }); +} + +/// Propagates disabling to `entity` and its descendants, stopping at [`OwnsDisabled`]. +fn propagate_disable(commands: &mut Commands, entity: Entity) { + commands.queue(move |world: &mut World| { + let mut stack = vec![entity]; + while let Some(current) = stack.pop() { + let Ok(mut entity_mut) = world.get_entity_mut(current) else { + continue; + }; + entity_mut.insert(Disabled); + let children: Vec = entity_mut + .get::() + .map(|c| c.iter().copied().collect()) + .unwrap_or_default(); + drop(entity_mut); + for child in children { + if world.get::(child).is_none() { + stack.push(child); + } + } + } + }); +} + #[cfg(test)] mod tests { use super::*; @@ -1333,11 +1442,11 @@ mod tests { app.update(); assert!(app.world().get::(entity).is_some()); - // cleanup observer should remove the disabled component + // Cleanup observer should remove the `OwnsDisabled` component. app.world_mut() .entity_mut(entity) .remove::>(); - assert!(app.world().get::(entity).is_none()); + assert!(app.world().get::(entity).is_none()); } #[test] @@ -1424,4 +1533,83 @@ mod tests { app.update(); assert!(app.world().get::(entity).is_some()); } + + #[test] + fn enabled_in_disabled_in_recursive() { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)] + enum State { + Off, + Limbo, + On, + } + + #[derive(Component)] + struct Entity1; + #[derive(Component)] + struct Entity2; + #[derive(Component)] + struct Entity3; + #[derive(Component)] + struct Entity4; + + fn is_disabled(world: &mut World) -> bool { + world + .query_filtered::<&Disabled, (With, Allow)>() + .single(world) + .is_ok() + } + + let mut app = App::new(); + app.add_plugins(StatesPlugin); + + app.insert_state(State::Off); + app.update(); + + /* + | Entity | Component | Off | Limbo | On | + |---------|---------------------|----------|----------|----------| + | Entity1 | `EnabledIn(On)` | disabled | disabled | enabled | + | Entity2 | `DisabledIn(On)` | enabled | enabled | disabled | + | Entity3 | `OwnsDisabled` | enabled | enabled | enabled | + | Entity4 | `DisabledIn(Limbo)` | enabled | disabled | enabled | + */ + + app.world_mut().spawn(( + Entity1, + EnabledIn(State::On), + bevy_ecs::children![( + Entity2, + DisabledIn(State::On), + bevy_ecs::children![( + Entity3, + OwnsDisabled, + bevy_ecs::children![(Entity4, DisabledIn(State::Limbo))] + )] + )], + )); + + // Initialized as State::Off + assert!(is_disabled::(app.world_mut())); + assert!(!is_disabled::(app.world_mut())); + assert!(!is_disabled::(app.world_mut())); + assert!(!is_disabled::(app.world_mut())); + + // Switch to State::Limbo + app.world_mut().commands().set_state(State::Limbo); + app.update(); + + assert!(is_disabled::(app.world_mut())); + assert!(!is_disabled::(app.world_mut())); + assert!(!is_disabled::(app.world_mut())); + assert!(is_disabled::(app.world_mut())); + + // Switch to State::On + app.world_mut().commands().set_state(State::On); + app.update(); + + assert!(!is_disabled::(app.world_mut())); + assert!(is_disabled::(app.world_mut())); + assert!(!is_disabled::(app.world_mut())); + assert!(!is_disabled::(app.world_mut())); + } } From 8333d945e27752a521feacb62c25818a6d9a415e Mon Sep 17 00:00:00 2001 From: izarma Date: Sun, 28 Jun 2026 20:09:56 +0530 Subject: [PATCH 6/9] DisabledSelf --- crates/bevy_state/src/lib.rs | 4 +- crates/bevy_state/src/state_scoped.rs | 159 +++++++++++++++++++++++--- 2 files changed, 142 insertions(+), 21 deletions(-) diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs index f0a329f86c5eb..6e78b565f5de5 100644 --- a/crates/bevy_state/src/lib.rs +++ b/crates/bevy_state/src/lib.rs @@ -94,8 +94,8 @@ pub mod prelude { }, state_scoped::{ DespawnOnEnter, DespawnOnExit, DespawnWhen, DisableOnEnter, DisableOnExit, DisableWhen, - DisabledIf, DisabledIn, EnableOnEnter, EnableOnExit, EnableWhen, EnabledIf, EnabledIn, - OwnsDisabled, + DisabledIf, DisabledIn, DisabledSelf, EnableOnEnter, EnableOnExit, EnableWhen, + EnabledIf, EnabledIn, OwnsDisabled, }, }; } diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index 4ce7feb5a4dd6..69cb2d239876f 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -761,12 +761,15 @@ pub fn enable_entities_on_enter_state( } /// Marks this entity as owning its [`Disabled`] state. -/// When present, parent-driven enable/disable propogation skips this entity +/// When present, parent-driven enable and disable propagation skips this entity /// and its descendants. /// /// Automatically required by state-driven disabling components -/// i.e. ([`EnabledIn`], [`DisabledIn`], [`EnabledIf`], [`DisabledIf`]). -/// Can be inserted manually to shield an entity from parent's disabled propagation. +/// i.e., ([`EnabledIn`], [`DisabledIn`], [`EnabledIf`], [`DisabledIf`]). +/// Can be inserted manually. +/// +/// If you only want to block re-enabling while still allowing a parent disable to +/// propagate to this entity, use [`DisabledSelf`] instead. /// ``` /// # use bevy_app::Startup; /// use bevy_state::prelude::*; @@ -791,7 +794,6 @@ pub fn enable_entities_on_enter_state( /// // This entity and its descendants will be ignored by parent state transitions. /// ShieldedChild, /// OwnsDisabled, -/// Disabled, /// )] /// )); /// } @@ -808,7 +810,7 @@ pub fn enable_entities_on_enter_state( /// app.add_systems(Startup, spawn_parent_entity); /// ``` /// -/// See also [`EnabledIn`], [`DisabledIn`], [`EnabledIf`], and [`DisabledIf`]. +/// See also [`DisabledSelf`], [`EnabledIn`], [`DisabledIn`], [`EnabledIf`], and [`DisabledIf`]. #[derive(Component, Clone, Default)] #[cfg_attr( feature = "bevy_reflect", @@ -817,6 +819,67 @@ pub fn enable_entities_on_enter_state( )] pub struct OwnsDisabled; +/// Marks this entity as independently disabled. +/// When present, parent-driven **enable** propagation skips this entity and its +/// descendants, but parent-driven **disable** propagation still applies. +/// +/// Use this when you want a child to be disabled together with its parent, but you +/// do not want the parent re-enabling to automatically clear [`Disabled`] from this +/// entity or its descendants. +/// +/// Note: state-driven disabling components require [`OwnsDisabled`], which blocks +/// both disable and enable propagation. If an entity has both [`OwnsDisabled`] and +/// [`DisabledSelf`], [`OwnsDisabled`] takes precedence. +/// ``` +/// # use bevy_app::Startup; +/// use bevy_state::prelude::*; +/// use bevy_ecs::{prelude::*, system::ScheduleSystem, entity_disabling::Disabled}; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// InGame, +/// } +/// +/// # #[derive(Component)] +/// # struct ParentEntity; +/// # #[derive(Component)] +/// # struct ShieldedChild; +/// fn spawn_parent_entity(mut commands: Commands) { +/// commands.spawn(( +/// ParentEntity, +/// EnabledIn(GameState::MainMenu), +/// children![( +/// // This entity will be disabled when its parent is disabled, but it +/// // will stay disabled when the parent is re-enabled. +/// ShieldedChild, +/// DisabledSelf, +/// )] +/// )); +/// } +/// +/// # struct AppMock; +/// # impl AppMock { +/// # fn init_state(&mut self) {} +/// # fn add_systems(&mut self, schedule: S, systems: impl IntoScheduleConfigs) {} +/// # } +/// # struct Update; +/// # let mut app = AppMock; +/// +/// app.init_state::(); +/// app.add_systems(Startup, spawn_parent_entity); +/// ``` +/// +/// See also [`OwnsDisabled`], [`EnabledIn`], [`DisabledIn`], [`EnabledIf`], and [`DisabledIf`]. +#[derive(Component, Clone, Default)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Component, Clone, Default) +)] +pub struct DisabledSelf; + /// Removes [`OwnsDisabled`] component from an entity /// when its managing state-driven disabling components are removed /// i.e. ([`EnabledIn`], [`DisabledIn`], [`EnabledIf`], [`DisabledIf`]). @@ -828,12 +891,12 @@ pub fn on_state_disabled_component_remove(on: On, mut c /// Entities marked with this component will be automatically enabled /// when the world is in the given state, and disabled otherwise. /// -/// This component takes ownership of adding or removing entity's [`Disabled`] component +/// This component takes ownership of adding or removing the entity's [`Disabled`] component /// at state transitions and component insertion. /// At component removal, [`Disabled`] is left as is. /// /// # Note -/// System is added on state registration, so `Res>` should always exist +/// System is added on state registration, so `Res>` should always exist. /// /// ``` /// # use bevy_app::Startup; @@ -927,12 +990,12 @@ pub fn on_enabled_in_insert( /// Entities marked with this component will be automatically disabled /// when the world is in the given state, and enabled otherwise. /// -/// This component takes ownership of adding or removing entity's [`Disabled`] component +/// This component takes ownership of adding or removing the entity's [`Disabled`] component /// at state transitions and component insertion. /// At component removal, [`Disabled`] is left as is. /// /// # Note -/// System is added on state registration, so `Res>` should always exist +/// System is added on state registration, so `Res>` should always exist. /// /// ``` /// # use bevy_app::Startup; @@ -1024,15 +1087,14 @@ pub fn on_disabled_in_insert( } /// Entities marked with this component will be automatically enabled -/// when predicate returns `true` for current state, disabled otherwise. +/// when the predicate returns `true` for the current state, disabled otherwise. /// -/// This component takes ownership of adding or removing entity's [`Disabled`] component +/// This component takes ownership of adding or removing the entity's [`Disabled`] component /// at state transitions and component insertion. /// At component removal, [`Disabled`] is left as is. /// /// # Note -/// System is added on state registration, so `Res>` should always exist -/// +/// System is added on state registration, so `Res>` should always exist. /// /// ``` /// # use bevy_app::Startup; @@ -1136,14 +1198,14 @@ pub fn on_enabled_if_insert( } /// Entities marked with this component will be automatically disabled -/// when predicate returns `true` for current state, enabled otherwise. +/// when the predicate returns `true` for the current state, enabled otherwise. /// -/// This component takes ownership of adding or removing entity's [`Disabled`] component +/// This component takes ownership of adding or removing the entity's [`Disabled`] component /// at state transitions and component insertion. /// At component removal, [`Disabled`] is left as is. /// /// # Note -/// System is added on state registration, so `Res>` should always exist +/// System is added on state registration, so `Res>` should always exist. /// /// ``` /// # use bevy_app::Startup; @@ -1247,7 +1309,8 @@ pub fn on_disabled_if_insert( } } -/// Propagates enabling to `entity` and its descendants, stopping at [`OwnsDisabled`]. +/// Propagates enabling to `entity` and its descendants, stopping at [`OwnsDisabled`] +/// and [`DisabledSelf`]. fn propagate_enable(commands: &mut Commands, entity: Entity) { commands.queue(move |world: &mut World| { let mut stack = vec![entity]; @@ -1262,7 +1325,9 @@ fn propagate_enable(commands: &mut Commands, entity: Entity) { .unwrap_or_default(); drop(entity_mut); for child in children { - if world.get::(child).is_none() { + if world.get::(child).is_none() + && world.get::(child).is_none() + { stack.push(child); } } @@ -1535,7 +1600,7 @@ mod tests { } #[test] - fn enabled_in_disabled_in_recursive() { + fn enabled_in_disabled_in_propagation() { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)] enum State { Off, @@ -1612,4 +1677,60 @@ mod tests { assert!(!is_disabled::(app.world_mut())); assert!(!is_disabled::(app.world_mut())); } + + #[test] + fn disabled_self_blocks_re_enable_but_not_disable() { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)] + enum State { + Off, + On, + } + + #[derive(Component)] + struct Parent; + #[derive(Component)] + struct Child; + #[derive(Component)] + struct Grandchild; + + fn is_disabled(world: &mut World) -> bool { + world + .query_filtered::<&Disabled, (With, Allow)>() + .single(world) + .is_ok() + } + + let mut app = App::new(); + app.add_plugins(StatesPlugin); + + app.insert_state(State::On); + app.update(); + + app.world_mut().spawn(( + Parent, + EnabledIn(State::On), + bevy_ecs::children![(Child, DisabledSelf, bevy_ecs::children![(Grandchild)])], + )); + + // In State::On the whole hierarchy is enabled. + assert!(!is_disabled::(app.world_mut())); + assert!(!is_disabled::(app.world_mut())); + assert!(!is_disabled::(app.world_mut())); + + // Switch to State::Off: parent is disabled, disable propagates through DisabledSelf. + app.world_mut().commands().set_state(State::Off); + app.update(); + + assert!(is_disabled::(app.world_mut())); + assert!(is_disabled::(app.world_mut())); + assert!(is_disabled::(app.world_mut())); + + // Switch back to State::On: parent is enabled, but DisabledSelf blocks re-enable. + app.world_mut().commands().set_state(State::On); + app.update(); + + assert!(!is_disabled::(app.world_mut())); + assert!(is_disabled::(app.world_mut())); + assert!(is_disabled::(app.world_mut())); + } } From f00de68eda8264e02cb8be5c0cab27af353c279a Mon Sep 17 00:00:00 2001 From: izarma Date: Sun, 28 Jun 2026 22:57:04 +0530 Subject: [PATCH 7/9] ci fix --- crates/bevy_state/src/state_scoped.rs | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index 24452f399e93d..c25e5d23fa76f 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -1323,7 +1323,6 @@ fn propagate_enable(commands: &mut Commands, entity: Entity) { .get::() .map(|c| c.iter().copied().collect()) .unwrap_or_default(); - drop(entity_mut); for child in children { if world.get::(child).is_none() && world.get::(child).is_none() @@ -1348,7 +1347,6 @@ fn propagate_disable(commands: &mut Commands, entity: Entity) { .get::() .map(|c| c.iter().copied().collect()) .unwrap_or_default(); - drop(entity_mut); for child in children { if world.get::(child).is_none() { stack.push(child); @@ -1369,6 +1367,13 @@ mod tests { prelude::CommandsStatesExt, }; + fn is_disabled(world: &mut World) -> bool { + world + .query_filtered::<&Disabled, (With, Allow)>() + .single(world) + .is_ok() + } + #[test] fn despawn_on_exit_from_computed_state() { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)] @@ -1617,13 +1622,6 @@ mod tests { #[derive(Component)] struct Entity4; - fn is_disabled(world: &mut World) -> bool { - world - .query_filtered::<&Disabled, (With, Allow)>() - .single(world) - .is_ok() - } - let mut app = App::new(); app.add_plugins(StatesPlugin); @@ -1693,13 +1691,6 @@ mod tests { #[derive(Component)] struct Grandchild; - fn is_disabled(world: &mut World) -> bool { - world - .query_filtered::<&Disabled, (With, Allow)>() - .single(world) - .is_ok() - } - let mut app = App::new(); app.add_plugins(StatesPlugin); From 92837a06b31c67b0be4830c477904f1695954252 Mon Sep 17 00:00:00 2001 From: izarma Date: Tue, 30 Jun 2026 15:06:43 +0530 Subject: [PATCH 8/9] update names --- crates/bevy_state/src/lib.rs | 4 +- crates/bevy_state/src/state_scoped.rs | 158 ++++++++++---------------- 2 files changed, 64 insertions(+), 98 deletions(-) diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs index 6e78b565f5de5..633e7a7231e01 100644 --- a/crates/bevy_state/src/lib.rs +++ b/crates/bevy_state/src/lib.rs @@ -94,8 +94,8 @@ pub mod prelude { }, state_scoped::{ DespawnOnEnter, DespawnOnExit, DespawnWhen, DisableOnEnter, DisableOnExit, DisableWhen, - DisabledIf, DisabledIn, DisabledSelf, EnableOnEnter, EnableOnExit, EnableWhen, - EnabledIf, EnabledIn, OwnsDisabled, + DisabledControl, DisabledIf, DisabledIn, EnableOnEnter, EnableOnExit, EnableWhen, + EnabledIf, EnabledIn, }, }; } diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index c25e5d23fa76f..ccf4bcc6b4001 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -760,76 +760,21 @@ pub fn enable_entities_on_enter_state( } } -/// Marks this entity as owning its [`Disabled`] state. -/// When present, parent-driven enable and disable propagation skips this entity +/// Controls how parent-driven disabling and enabling propagate to this entity /// and its descendants. /// /// Automatically required by state-driven disabling components -/// i.e., ([`EnabledIn`], [`DisabledIn`], [`EnabledIf`], [`DisabledIf`]). -/// Can be inserted manually. -/// -/// If you only want to block re-enabling while still allowing a parent disable to -/// propagate to this entity, use [`DisabledSelf`] instead. -/// ``` -/// # use bevy_app::Startup; -/// use bevy_state::prelude::*; -/// use bevy_ecs::{prelude::*, system::ScheduleSystem, entity_disabling::Disabled}; -/// -/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] -/// enum GameState { -/// #[default] -/// MainMenu, -/// InGame, -/// } -/// -/// # #[derive(Component)] -/// # struct ParentEntity; -/// # #[derive(Component)] -/// # struct ShieldedChild; -/// fn spawn_parent_entity(mut commands: Commands) { -/// commands.spawn(( -/// ParentEntity, -/// EnabledIn(GameState::MainMenu), -/// children![( -/// // This entity and its descendants will be ignored by parent state transitions. -/// ShieldedChild, -/// OwnsDisabled, -/// )] -/// )); -/// } -/// -/// # struct AppMock; -/// # impl AppMock { -/// # fn init_state(&mut self) {} -/// # fn add_systems(&mut self, schedule: S, systems: impl IntoScheduleConfigs) {} -/// # } -/// # struct Update; -/// # let mut app = AppMock; -/// -/// app.init_state::(); -/// app.add_systems(Startup, spawn_parent_entity); -/// ``` -/// -/// See also [`DisabledSelf`], [`EnabledIn`], [`DisabledIn`], [`EnabledIf`], and [`DisabledIf`]. -#[derive(Component, Clone, Default)] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Component, Clone, Default) -)] -pub struct OwnsDisabled; - -/// Marks this entity as independently disabled. -/// When present, parent-driven **enable** propagation skips this entity and its -/// descendants, but parent-driven **disable** propagation still applies. -/// -/// Use this when you want a child to be disabled together with its parent, but you -/// do not want the parent re-enabling to automatically clear [`Disabled`] from this -/// entity or its descendants. -/// -/// Note: state-driven disabling components require [`OwnsDisabled`], which blocks -/// both disable and enable propagation. If an entity has both [`OwnsDisabled`] and -/// [`DisabledSelf`], [`OwnsDisabled`] takes precedence. +/// i.e., ([`EnabledIn`], [`DisabledIn`], [`EnabledIf`], [`DisabledIf`]), +/// defaulting to [`DisabledControl::Independent`]. +/// Can also be inserted manually. +/// +/// # Variants +/// - [`Independent`](DisabledControl::Independent): parent-driven disable **and** +/// enable propagation both stop at this entity. +/// - [`InheritDisable`](DisabledControl::InheritDisable): parent-driven disable still +/// propagates, but parent-driven enable does not. Use this when you want a child +/// to be disabled with its parent, but not automatically re-enabled when the +/// parent is reactivated. /// ``` /// # use bevy_app::Startup; /// use bevy_state::prelude::*; @@ -854,7 +799,7 @@ pub struct OwnsDisabled; /// // This entity will be disabled when its parent is disabled, but it /// // will stay disabled when the parent is re-enabled. /// ShieldedChild, -/// DisabledSelf, +/// DisabledControl::InheritDisable, /// )] /// )); /// } @@ -871,21 +816,27 @@ pub struct OwnsDisabled; /// app.add_systems(Startup, spawn_parent_entity); /// ``` /// -/// See also [`OwnsDisabled`], [`EnabledIn`], [`DisabledIn`], [`EnabledIf`], and [`DisabledIf`]. -#[derive(Component, Clone, Default)] +/// See also [`EnabledIn`], [`DisabledIn`], [`EnabledIf`], and [`DisabledIf`]. +#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), - reflect(Component, Clone, Default) + reflect(Component, Clone, Debug, Default, PartialEq) )] -pub struct DisabledSelf; +pub enum DisabledControl { + /// Parent-driven disable and enable propagation both stop at this entity. + #[default] + Independent, + /// Parent-driven disable propagates, but parent-driven enable does not. + InheritDisable, +} -/// Removes [`OwnsDisabled`] component from an entity +/// Removes [`DisabledControl`] component from an entity /// when its managing state-driven disabling components are removed /// i.e. ([`EnabledIn`], [`DisabledIn`], [`EnabledIf`], [`DisabledIf`]). pub fn on_state_disabled_component_remove(on: On, mut commands: Commands) { let mut entity = commands.entity(on.entity); - entity.remove::(); + entity.remove::(); } /// Entities marked with this component will be automatically enabled @@ -893,6 +844,9 @@ pub fn on_state_disabled_component_remove(on: On, mut c /// /// This component takes ownership of adding or removing the entity's [`Disabled`] component /// at state transitions and component insertion. +/// The [`Disabled`] component propagates to children entities by default. +/// It requires [`DisabledControl`], defaulting to [`DisabledControl::Independent`], +/// so parent-driven disable/enable propagation stops here. /// At component removal, [`Disabled`] is left as is. /// /// # Note @@ -933,7 +887,7 @@ pub fn on_state_disabled_component_remove(on: On, mut c /// Use [`EnableOnEnter`] and [`DisableOnExit`] separately if you need finer control. /// See also [`EnabledIf`] and [`DisabledIn`]. #[derive(Component, Clone)] -#[require(OwnsDisabled)] +#[require(DisabledControl)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))] pub struct EnabledIn(pub S); @@ -992,6 +946,9 @@ pub fn on_enabled_in_insert( /// /// This component takes ownership of adding or removing the entity's [`Disabled`] component /// at state transitions and component insertion. +/// The [`Disabled`] component propagates to children entities by default. +/// It requires [`DisabledControl`], defaulting to [`DisabledControl::Independent`], +/// so parent-driven disable/enable propagation stops here. /// At component removal, [`Disabled`] is left as is. /// /// # Note @@ -1032,7 +989,7 @@ pub fn on_enabled_in_insert( /// Use [`DisableOnEnter`] and [`EnableOnExit`] separately if you need finer control. /// See also [`DisabledIf`] and [`EnabledIn`]. #[derive(Component, Clone)] -#[require(OwnsDisabled)] +#[require(DisabledControl)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))] pub struct DisabledIn(pub S); @@ -1091,6 +1048,9 @@ pub fn on_disabled_in_insert( /// /// This component takes ownership of adding or removing the entity's [`Disabled`] component /// at state transitions and component insertion. +/// The [`Disabled`] component propagates to children entities by default. +/// It requires [`DisabledControl`], defaulting to [`DisabledControl::Independent`], +/// so parent-driven disable/enable propagation stops here. /// At component removal, [`Disabled`] is left as is. /// /// # Note @@ -1133,7 +1093,7 @@ pub fn on_disabled_in_insert( /// /// See also [`DisabledIf`] and [`EnabledIn`]. #[derive(Component)] -#[require(OwnsDisabled)] +#[require(DisabledControl)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] pub struct EnabledIf { /// The predicate used to determine if the entity should be enabled for the given state. @@ -1202,6 +1162,9 @@ pub fn on_enabled_if_insert( /// /// This component takes ownership of adding or removing the entity's [`Disabled`] component /// at state transitions and component insertion. +/// The [`Disabled`] component propagates to children entities by default. +/// It requires [`DisabledControl`], defaulting to [`DisabledControl::Independent`], +/// so parent-driven disable/enable propagation stops here. /// At component removal, [`Disabled`] is left as is. /// /// # Note @@ -1245,7 +1208,7 @@ pub fn on_enabled_if_insert( /// /// See also [`EnabledIf`] and [`DisabledIn`]. #[derive(Component)] -#[require(OwnsDisabled)] +#[require(DisabledControl)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] pub struct DisabledIf { /// The predicate used to determine if the entity should be disabled for the given state. @@ -1309,8 +1272,7 @@ pub fn on_disabled_if_insert( } } -/// Propagates enabling to `entity` and its descendants, stopping at [`OwnsDisabled`] -/// and [`DisabledSelf`]. +/// Propagates enabling to `entity` and its descendants, stopping at [`DisabledControl`]. fn propagate_enable(commands: &mut Commands, entity: Entity) { commands.queue(move |world: &mut World| { let mut stack = vec![entity]; @@ -1324,8 +1286,8 @@ fn propagate_enable(commands: &mut Commands, entity: Entity) { .map(|c| c.iter().copied().collect()) .unwrap_or_default(); for child in children { - if world.get::(child).is_none() - && world.get::(child).is_none() + if world.get::(child) != Some(&DisabledControl::Independent) + && world.get::(child) != Some(&DisabledControl::InheritDisable) { stack.push(child); } @@ -1334,7 +1296,7 @@ fn propagate_enable(commands: &mut Commands, entity: Entity) { }); } -/// Propagates disabling to `entity` and its descendants, stopping at [`OwnsDisabled`]. +/// Propagates disabling to `entity` and its descendants, stopping at [`DisabledControl::Independent`]. fn propagate_disable(commands: &mut Commands, entity: Entity) { commands.queue(move |world: &mut World| { let mut stack = vec![entity]; @@ -1348,7 +1310,7 @@ fn propagate_disable(commands: &mut Commands, entity: Entity) { .map(|c| c.iter().copied().collect()) .unwrap_or_default(); for child in children { - if world.get::(child).is_none() { + if world.get::(child) != Some(&DisabledControl::Independent) { stack.push(child); } } @@ -1512,11 +1474,11 @@ mod tests { app.update(); assert!(app.world().get::(entity).is_some()); - // Cleanup observer should remove the `OwnsDisabled` component. + // Cleanup observer should remove the `DisabledControl` component. app.world_mut() .entity_mut(entity) .remove::>(); - assert!(app.world().get::(entity).is_none()); + assert!(app.world().get::(entity).is_none()); } #[test] @@ -1629,12 +1591,12 @@ mod tests { app.update(); /* - | Entity | Component | Off | Limbo | On | - |---------|---------------------|----------|----------|----------| - | Entity1 | `EnabledIn(On)` | disabled | disabled | enabled | - | Entity2 | `DisabledIn(On)` | enabled | enabled | disabled | - | Entity3 | `OwnsDisabled` | enabled | enabled | enabled | - | Entity4 | `DisabledIn(Limbo)` | enabled | disabled | enabled | + | Entity | Component | Off | Limbo | On | + |---------|--------------------------------|----------|----------|----------| + | Entity1 | `EnabledIn(On)` | disabled | disabled | enabled | + | Entity2 | `DisabledIn(On)` | enabled | enabled | disabled | + | Entity3 | `DisabledControl::Independent` | enabled | enabled | enabled | + | Entity4 | `DisabledIn(Limbo)` | enabled | disabled | enabled | */ app.world_mut().spawn(( @@ -1645,7 +1607,7 @@ mod tests { DisabledIn(State::On), bevy_ecs::children![( Entity3, - OwnsDisabled, + DisabledControl::Independent, bevy_ecs::children![(Entity4, DisabledIn(State::Limbo))] )] )], @@ -1700,7 +1662,11 @@ mod tests { app.world_mut().spawn(( Parent, EnabledIn(State::On), - bevy_ecs::children![(Child, DisabledSelf, bevy_ecs::children![(Grandchild)])], + bevy_ecs::children![( + Child, + DisabledControl::InheritDisable, + bevy_ecs::children![(Grandchild)] + )], )); // In State::On the whole hierarchy is enabled. @@ -1708,7 +1674,7 @@ mod tests { assert!(!is_disabled::(app.world_mut())); assert!(!is_disabled::(app.world_mut())); - // Switch to State::Off: parent is disabled, disable propagates through DisabledSelf. + // Switch to State::Off: parent is disabled, disable propagates through DisabledControl::InheritDisable. app.world_mut().commands().set_state(State::Off); app.update(); @@ -1716,7 +1682,7 @@ mod tests { assert!(is_disabled::(app.world_mut())); assert!(is_disabled::(app.world_mut())); - // Switch back to State::On: parent is enabled, but DisabledSelf blocks re-enable. + // Switch back to State::On: parent is enabled, but DisabledControl::InheritDisable blocks re-enable. app.world_mut().commands().set_state(State::On); app.update(); From 637f389796e683986fd9596394272e9bff24ebf4 Mon Sep 17 00:00:00 2001 From: izarma Date: Fri, 3 Jul 2026 03:02:29 +0530 Subject: [PATCH 9/9] cleanup --- crates/bevy_state/src/state_scoped.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index ccf4bcc6b4001..e7e0e11b4bfac 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -817,11 +817,11 @@ pub fn enable_entities_on_enter_state( /// ``` /// /// See also [`EnabledIn`], [`DisabledIn`], [`EnabledIf`], and [`DisabledIf`]. -#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq)] +#[derive(Component, Debug, Default, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), - reflect(Component, Clone, Debug, Default, PartialEq) + reflect(Component, Debug, Default, PartialEq) )] pub enum DisabledControl { /// Parent-driven disable and enable propagation both stop at this entity. @@ -1286,9 +1286,7 @@ fn propagate_enable(commands: &mut Commands, entity: Entity) { .map(|c| c.iter().copied().collect()) .unwrap_or_default(); for child in children { - if world.get::(child) != Some(&DisabledControl::Independent) - && world.get::(child) != Some(&DisabledControl::InheritDisable) - { + if world.get::(child).is_none() { stack.push(child); } } @@ -1607,7 +1605,7 @@ mod tests { DisabledIn(State::On), bevy_ecs::children![( Entity3, - DisabledControl::Independent, + DisabledControl::default(), bevy_ecs::children![(Entity4, DisabledIn(State::Limbo))] )] )],