diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37db848558..d2410b57d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -293,7 +293,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.32.0 + uses: crate-ci/typos@v1.33.1 - name: Typos info if: failure() run: | diff --git a/Cargo.toml b/Cargo.toml index 703877983c..32891dd1be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,7 @@ default = [ "bevy_audio", "bevy_color", "bevy_core_pipeline", + "bevy_core_widgets", "bevy_anti_aliasing", "bevy_gilrs", "bevy_gizmos", @@ -292,6 +293,9 @@ bevy_log = ["bevy_internal/bevy_log"] # Enable input focus subsystem bevy_input_focus = ["bevy_internal/bevy_input_focus"] +# Headless widget collection for Bevy UI. +bevy_core_widgets = ["bevy_internal/bevy_core_widgets"] + # Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation) spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"] @@ -4438,3 +4442,25 @@ name = "Hotpatching Systems" description = "Demonstrates how to hotpatch systems" category = "ECS (Entity Component System)" wasm = false + +[[example]] +name = "core_widgets" +path = "examples/ui/core_widgets.rs" +doc-scrape-examples = true + +[package.metadata.example.core_widgets] +name = "Core Widgets" +description = "Demonstrates use of core (headless) widgets in Bevy UI" +category = "UI (User Interface)" +wasm = true + +[[example]] +name = "core_widgets_observers" +path = "examples/ui/core_widgets_observers.rs" +doc-scrape-examples = true + +[package.metadata.example.core_widgets_observers] +name = "Core Widgets (w/Observers)" +description = "Demonstrates use of core (headless) widgets in Bevy UI, with Observers" +category = "UI (User Interface)" +wasm = true diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 61bbad3aed..2adf6c2857 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -1483,8 +1483,8 @@ mod tests { component::Component, entity::Entity, event::{Event, EventWriter, Events}, + lifecycle::RemovedComponents, query::With, - removal_detection::RemovedComponents, resource::Resource, schedule::{IntoScheduleConfigs, ScheduleLabel}, system::{Commands, Query}, diff --git a/crates/bevy_app/src/propagate.rs b/crates/bevy_app/src/propagate.rs index 5d766a626c..754ba3140e 100644 --- a/crates/bevy_app/src/propagate.rs +++ b/crates/bevy_app/src/propagate.rs @@ -6,9 +6,9 @@ use bevy_ecs::{ component::Component, entity::Entity, hierarchy::ChildOf, + lifecycle::RemovedComponents, query::{Changed, Or, QueryFilter, With, Without}, relationship::{Relationship, RelationshipTarget}, - removal_detection::RemovedComponents, schedule::{IntoScheduleConfigs, SystemSet}, system::{Commands, Local, Query}, }; diff --git a/crates/bevy_audio/Cargo.toml b/crates/bevy_audio/Cargo.toml index aff7f83b37..ae5385870d 100644 --- a/crates/bevy_audio/Cargo.toml +++ b/crates/bevy_audio/Cargo.toml @@ -19,12 +19,18 @@ bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } # other +# TODO: Remove `coreaudio-sys` dep below when updating `cpal`. rodio = { version = "0.20", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } [target.'cfg(target_os = "android")'.dependencies] cpal = { version = "0.15", optional = true } +[target.'cfg(target_vendor = "apple")'.dependencies] +# NOTE: Explicitly depend on this patch version to fix: +# https://github.com/bevyengine/bevy/issues/18893 +coreaudio-sys = { version = "0.2.17", default-features = false } + [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. rodio = { version = "0.20", default-features = false, features = [ diff --git a/crates/bevy_core_pipeline/src/oit/mod.rs b/crates/bevy_core_pipeline/src/oit/mod.rs index 3ae95d71cc..5b5d038fa0 100644 --- a/crates/bevy_core_pipeline/src/oit/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/mod.rs @@ -1,7 +1,7 @@ //! Order Independent Transparency (OIT) for 3d rendering. See [`OrderIndependentTransparencyPlugin`] for more details. use bevy_app::prelude::*; -use bevy_ecs::{component::*, prelude::*}; +use bevy_ecs::{component::*, lifecycle::ComponentHook, prelude::*}; use bevy_math::UVec2; use bevy_platform::collections::HashSet; use bevy_platform::time::Instant; diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml new file mode 100644 index 0000000000..21540a9787 --- /dev/null +++ b/crates/bevy_core_widgets/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "bevy_core_widgets" +version = "0.16.0-dev" +edition = "2024" +description = "Unstyled common widgets for B Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } +bevy_a11y = { path = "../bevy_a11y", version = "0.16.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" } +bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev" } + +# other +accesskit = "0.19" + +[features] +default = [] + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs new file mode 100644 index 0000000000..bf88c27143 --- /dev/null +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -0,0 +1,141 @@ +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::query::Has; +use bevy_ecs::system::ResMut; +use bevy_ecs::{ + component::Component, + entity::Entity, + observer::Trigger, + query::With, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; +use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; +use bevy_ui::{InteractionDisabled, Pressed}; + +/// Headless button widget. This widget maintains a "pressed" state, which is used to +/// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked` +/// event when the button is un-pressed. +#[derive(Component, Debug)] +#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))] +pub struct CoreButton { + /// Optional system to run when the button is clicked, or when the Enter or Space key + /// is pressed while the button is focused. If this field is `None`, the button will + /// emit a `ButtonClicked` event when clicked. + pub on_click: Option, +} + +fn button_on_key_event( + mut trigger: Trigger>, + q_state: Query<(&CoreButton, Has)>, + mut commands: Commands, +) { + if let Ok((bstate, disabled)) = q_state.get(trigger.target().unwrap()) { + if !disabled { + let event = &trigger.event().input; + if !event.repeat + && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) + { + if let Some(on_click) = bstate.on_click { + trigger.propagate(false); + commands.run_system(on_click); + } + } + } + } +} + +fn button_on_pointer_click( + mut trigger: Trigger>, + mut q_state: Query<(&CoreButton, Has, Has)>, + mut commands: Commands, +) { + if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target().unwrap()) { + trigger.propagate(false); + if pressed && !disabled { + if let Some(on_click) = bstate.on_click { + commands.run_system(on_click); + } + } + } +} + +fn button_on_pointer_down( + mut trigger: Trigger>, + mut q_state: Query<(Entity, Has, Has), With>, + focus: Option>, + focus_visible: Option>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) { + trigger.propagate(false); + if !disabled { + if !pressed { + commands.entity(button).insert(Pressed); + } + // Clicking on a button makes it the focused input, + // and hides the focus ring if it was visible. + if let Some(mut focus) = focus { + focus.0 = trigger.target(); + } + if let Some(mut focus_visible) = focus_visible { + focus_visible.0 = false; + } + } + } +} + +fn button_on_pointer_up( + mut trigger: Trigger>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) { + trigger.propagate(false); + if !disabled && pressed { + commands.entity(button).remove::(); + } + } +} + +fn button_on_pointer_drag_end( + mut trigger: Trigger>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) { + trigger.propagate(false); + if !disabled && pressed { + commands.entity(button).remove::(); + } + } +} + +fn button_on_pointer_cancel( + mut trigger: Trigger>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) { + trigger.propagate(false); + if !disabled && pressed { + commands.entity(button).remove::(); + } + } +} + +/// Plugin that adds the observers for the [`CoreButton`] widget. +pub struct CoreButtonPlugin; + +impl Plugin for CoreButtonPlugin { + fn build(&self, app: &mut App) { + app.add_observer(button_on_key_event) + .add_observer(button_on_pointer_down) + .add_observer(button_on_pointer_up) + .add_observer(button_on_pointer_click) + .add_observer(button_on_pointer_drag_end) + .add_observer(button_on_pointer_cancel); + } +} diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs new file mode 100644 index 0000000000..afeed92a1f --- /dev/null +++ b/crates/bevy_core_widgets/src/lib.rs @@ -0,0 +1,27 @@ +//! This crate provides a set of core widgets for Bevy UI, such as buttons, checkboxes, and sliders. +//! These widgets have no inherent styling, it's the responsibility of the user to add styling +//! appropriate for their game or application. +//! +//! # State Management +//! +//! Most of the widgets use external state management: this means that the widgets do not +//! automatically update their own internal state, but instead rely on the app to update the widget +//! state (as well as any other related game state) in response to a change event emitted by the +//! widget. The primary motivation for this is to avoid two-way data binding in scenarios where the +//! user interface is showing a live view of dynamic data coming from deeper within the game engine. + +mod core_button; + +use bevy_app::{App, Plugin}; + +pub use core_button::{CoreButton, CoreButtonPlugin}; + +/// A plugin that registers the observers for all of the core widgets. If you don't want to +/// use all of the widgets, you can import the individual widget plugins instead. +pub struct CoreWidgetsPlugin; + +impl Plugin for CoreWidgetsPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(CoreButtonPlugin); + } +} diff --git a/crates/bevy_dev_tools/src/picking_debug.rs b/crates/bevy_dev_tools/src/picking_debug.rs index 79c1c8fff4..d11818dc6a 100644 --- a/crates/bevy_dev_tools/src/picking_debug.rs +++ b/crates/bevy_dev_tools/src/picking_debug.rs @@ -94,8 +94,8 @@ impl Plugin for DebugPickingPlugin { log_event_debug::.run_if(DebugPickingMode::is_noisy), log_pointer_event_debug::, log_pointer_event_debug::, - log_pointer_event_debug::, - log_pointer_event_debug::, + log_pointer_event_debug::, + log_pointer_event_debug::, log_pointer_event_debug::, log_pointer_event_trace::.run_if(DebugPickingMode::is_noisy), log_pointer_event_debug::, diff --git a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs index 4076819ee3..79c2158644 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs +++ b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs @@ -60,4 +60,4 @@ mod case4 { pub struct BarTargetOf(Entity); } -fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::component::HookContext) {} +fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::lifecycle::HookContext) {} diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index 00268cb680..6a693f2ce5 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -434,7 +434,7 @@ impl HookAttributeKind { HookAttributeKind::Path(path) => path.to_token_stream(), HookAttributeKind::Call(call) => { quote!({ - fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::component::HookContext) { + fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::lifecycle::HookContext) { (#call)(world, ctx) } _internal_hook @@ -658,7 +658,7 @@ fn hook_register_function_call( ) -> Option { function.map(|meta| { quote! { - fn #hook() -> ::core::option::Option<#bevy_ecs_path::component::ComponentHook> { + fn #hook() -> ::core::option::Option<#bevy_ecs_path::lifecycle::ComponentHook> { ::core::option::Option::Some(#meta) } } diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 133e5e9eaa..0a6f9b8884 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -29,6 +29,20 @@ enum BundleFieldKind { const BUNDLE_ATTRIBUTE_NAME: &str = "bundle"; const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore"; +const BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS: &str = "ignore_from_components"; + +#[derive(Debug)] +struct BundleAttributes { + impl_from_components: bool, +} + +impl Default for BundleAttributes { + fn default() -> Self { + Self { + impl_from_components: true, + } + } +} /// Implement the `Bundle` trait. #[proc_macro_derive(Bundle, attributes(bundle))] @@ -36,6 +50,27 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let ecs_path = bevy_ecs_path(); + let mut errors = vec![]; + + let mut attributes = BundleAttributes::default(); + + for attr in &ast.attrs { + if attr.path().is_ident(BUNDLE_ATTRIBUTE_NAME) { + let parsing = attr.parse_nested_meta(|meta| { + if meta.path.is_ident(BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS) { + attributes.impl_from_components = false; + return Ok(()); + } + + Err(meta.error(format!("Invalid bundle container attribute. Allowed attributes: `{BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS}`"))) + }); + + if let Err(error) = parsing { + errors.push(error.into_compile_error()); + } + } + } + let named_fields = match get_struct_fields(&ast.data, "derive(Bundle)") { Ok(fields) => fields, Err(e) => return e.into_compile_error().into(), @@ -130,7 +165,28 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let struct_name = &ast.ident; + let from_components = attributes.impl_from_components.then(|| quote! { + // SAFETY: + // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order + #[allow(deprecated)] + unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause { + #[allow(unused_variables, non_snake_case)] + unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self + where + __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_> + { + Self{ + #(#field_from_components)* + } + } + } + }); + + let attribute_errors = &errors; + TokenStream::from(quote! { + #(#attribute_errors)* + // SAFETY: // - ComponentId is returned in field-definition-order. [get_components] uses field-definition-order // - `Bundle::get_components` is exactly once for each member. Rely's on the Component -> Bundle implementation to properly pass @@ -159,20 +215,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { } } - // SAFETY: - // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order - #[allow(deprecated)] - unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause { - #[allow(unused_variables, non_snake_case)] - unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self - where - __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_> - { - Self{ - #(#field_from_components)* - } - } - } + #from_components #[allow(deprecated)] impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause { diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 4e5b28dde8..7369028715 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -693,7 +693,7 @@ impl Archetype { /// Returns true if any of the components in this archetype have at least one [`OnAdd`] observer /// - /// [`OnAdd`]: crate::world::OnAdd + /// [`OnAdd`]: crate::lifecycle::OnAdd #[inline] pub fn has_add_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_ADD_OBSERVER) @@ -701,7 +701,7 @@ impl Archetype { /// Returns true if any of the components in this archetype have at least one [`OnInsert`] observer /// - /// [`OnInsert`]: crate::world::OnInsert + /// [`OnInsert`]: crate::lifecycle::OnInsert #[inline] pub fn has_insert_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER) @@ -709,7 +709,7 @@ impl Archetype { /// Returns true if any of the components in this archetype have at least one [`OnReplace`] observer /// - /// [`OnReplace`]: crate::world::OnReplace + /// [`OnReplace`]: crate::lifecycle::OnReplace #[inline] pub fn has_replace_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_REPLACE_OBSERVER) @@ -717,7 +717,7 @@ impl Archetype { /// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer /// - /// [`OnRemove`]: crate::world::OnRemove + /// [`OnRemove`]: crate::lifecycle::OnRemove #[inline] pub fn has_remove_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER) @@ -725,7 +725,7 @@ impl Archetype { /// Returns true if any of the components in this archetype have at least one [`OnDespawn`] observer /// - /// [`OnDespawn`]: crate::world::OnDespawn + /// [`OnDespawn`]: crate::lifecycle::OnDespawn #[inline] pub fn has_despawn_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_DESPAWN_OBSERVER) diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index d7eb707543..7d6c0129dc 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -2,6 +2,57 @@ //! //! This module contains the [`Bundle`] trait and some other helper types. +/// Derive the [`Bundle`] trait +/// +/// You can apply this derive macro to structs that are +/// composed of [`Component`]s or +/// other [`Bundle`]s. +/// +/// ## Attributes +/// +/// Sometimes parts of the Bundle should not be inserted. +/// Those can be marked with `#[bundle(ignore)]`, and they will be skipped. +/// In that case, the field needs to implement [`Default`] unless you also ignore +/// the [`BundleFromComponents`] implementation. +/// +/// ```rust +/// # use bevy_ecs::prelude::{Component, Bundle}; +/// # #[derive(Component)] +/// # struct Hitpoint; +/// # +/// #[derive(Bundle)] +/// struct HitpointMarker { +/// hitpoints: Hitpoint, +/// +/// #[bundle(ignore)] +/// creator: Option +/// } +/// ``` +/// +/// Some fields may be bundles that do not implement +/// [`BundleFromComponents`]. This happens for bundles that cannot be extracted. +/// For example with [`SpawnRelatedBundle`](bevy_ecs::spawn::SpawnRelatedBundle), see below for an +/// example usage. +/// In those cases you can either ignore it as above, +/// or you can opt out the whole Struct by marking it as ignored with +/// `#[bundle(ignore_from_components)]`. +/// +/// ```rust +/// # use bevy_ecs::prelude::{Component, Bundle, ChildOf, Spawn}; +/// # #[derive(Component)] +/// # struct Hitpoint; +/// # #[derive(Component)] +/// # struct Marker; +/// # +/// use bevy_ecs::spawn::SpawnRelatedBundle; +/// +/// #[derive(Bundle)] +/// #[bundle(ignore_from_components)] +/// struct HitpointMarker { +/// hitpoints: Hitpoint, +/// related_spawner: SpawnRelatedBundle>, +/// } +/// ``` pub use bevy_ecs_macros::Bundle; use crate::{ @@ -15,15 +66,13 @@ use crate::{ RequiredComponents, StorageType, Tick, }, entity::{Entities, Entity, EntityLocation}, + lifecycle::{ON_ADD, ON_INSERT, ON_REMOVE, ON_REPLACE}, observer::Observers, prelude::World, query::DebugCheckedUnwrap, relationship::RelationshipHookMode, storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow}, - world::{ - unsafe_world_cell::UnsafeWorldCell, EntityWorldMut, ON_ADD, ON_INSERT, ON_REMOVE, - ON_REPLACE, - }, + world::{unsafe_world_cell::UnsafeWorldCell, EntityWorldMut}, }; use alloc::{boxed::Box, vec, vec::Vec}; use bevy_platform::collections::{HashMap, HashSet}; @@ -2072,7 +2121,7 @@ fn sorted_remove(source: &mut Vec, remove: &[T]) { #[cfg(test)] mod tests { use crate::{ - archetype::ArchetypeCreated, component::HookContext, prelude::*, world::DeferredWorld, + archetype::ArchetypeCreated, lifecycle::HookContext, prelude::*, world::DeferredWorld, }; use alloc::vec; @@ -2122,6 +2171,26 @@ mod tests { } } + #[derive(Bundle)] + #[bundle(ignore_from_components)] + struct BundleNoExtract { + b: B, + no_from_comp: crate::spawn::SpawnRelatedBundle>, + } + + #[test] + fn can_spawn_bundle_without_extract() { + let mut world = World::new(); + let id = world + .spawn(BundleNoExtract { + b: B, + no_from_comp: Children::spawn(Spawn(C)), + }) + .id(); + + assert!(world.entity(id).get::().is_some()); + } + #[test] fn component_hook_order_spawn_despawn() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index d083901ccc..338050f5fb 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -5,16 +5,17 @@ use crate::{ bundle::BundleInfo, change_detection::{MaybeLocation, MAX_CHANGE_AGE}, entity::{ComponentCloneCtx, Entity, EntityMapper, SourceComponent}, + lifecycle::{ComponentHook, ComponentHooks}, query::DebugCheckedUnwrap, - relationship::RelationshipHookMode, resource::Resource, storage::{SparseSetIndex, SparseSets, Table, TableRow}, system::{Local, SystemParam}, - world::{DeferredWorld, FromWorld, World}, + world::{FromWorld, World}, }; use alloc::boxed::Box; use alloc::{borrow::Cow, format, vec::Vec}; pub use bevy_ecs_macros::Component; +use bevy_ecs_macros::Event; use bevy_platform::sync::Arc; use bevy_platform::{ collections::{HashMap, HashSet}, @@ -375,7 +376,8 @@ use thiserror::Error; /// - `#[component(on_remove = on_remove_function)]` /// /// ``` -/// # use bevy_ecs::component::{Component, HookContext}; +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::lifecycle::HookContext; /// # use bevy_ecs::world::DeferredWorld; /// # use bevy_ecs::entity::Entity; /// # use bevy_ecs::component::ComponentId; @@ -404,7 +406,8 @@ use thiserror::Error; /// This also supports function calls that yield closures /// /// ``` -/// # use bevy_ecs::component::{Component, HookContext}; +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::lifecycle::HookContext; /// # use bevy_ecs::world::DeferredWorld; /// # /// #[derive(Component)] @@ -656,244 +659,6 @@ pub enum StorageType { SparseSet, } -/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove`. -pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, HookContext); - -/// Context provided to a [`ComponentHook`]. -#[derive(Clone, Copy, Debug)] -pub struct HookContext { - /// The [`Entity`] this hook was invoked for. - pub entity: Entity, - /// The [`ComponentId`] this hook was invoked for. - pub component_id: ComponentId, - /// The caller location is `Some` if the `track_caller` feature is enabled. - pub caller: MaybeLocation, - /// Configures how relationship hooks will run - pub relationship_hook_mode: RelationshipHookMode, -} - -/// [`World`]-mutating functions that run as part of lifecycle events of a [`Component`]. -/// -/// Hooks are functions that run when a component is added, overwritten, or removed from an entity. -/// These are intended to be used for structural side effects that need to happen when a component is added or removed, -/// and are not intended for general-purpose logic. -/// -/// For example, you might use a hook to update a cached index when a component is added, -/// to clean up resources when a component is removed, -/// or to keep hierarchical data structures across entities in sync. -/// -/// This information is stored in the [`ComponentInfo`] of the associated component. -/// -/// There is two ways of configuring hooks for a component: -/// 1. Defining the relevant hooks on the [`Component`] implementation -/// 2. Using the [`World::register_component_hooks`] method -/// -/// # Example 2 -/// -/// ``` -/// use bevy_ecs::prelude::*; -/// use bevy_platform::collections::HashSet; -/// -/// #[derive(Component)] -/// struct MyTrackedComponent; -/// -/// #[derive(Resource, Default)] -/// struct TrackedEntities(HashSet); -/// -/// let mut world = World::new(); -/// world.init_resource::(); -/// -/// // No entities with `MyTrackedComponent` have been added yet, so we can safely add component hooks -/// let mut tracked_component_query = world.query::<&MyTrackedComponent>(); -/// assert!(tracked_component_query.iter(&world).next().is_none()); -/// -/// world.register_component_hooks::().on_add(|mut world, context| { -/// let mut tracked_entities = world.resource_mut::(); -/// tracked_entities.0.insert(context.entity); -/// }); -/// -/// world.register_component_hooks::().on_remove(|mut world, context| { -/// let mut tracked_entities = world.resource_mut::(); -/// tracked_entities.0.remove(&context.entity); -/// }); -/// -/// let entity = world.spawn(MyTrackedComponent).id(); -/// let tracked_entities = world.resource::(); -/// assert!(tracked_entities.0.contains(&entity)); -/// -/// world.despawn(entity); -/// let tracked_entities = world.resource::(); -/// assert!(!tracked_entities.0.contains(&entity)); -/// ``` -#[derive(Debug, Clone, Default)] -pub struct ComponentHooks { - pub(crate) on_add: Option, - pub(crate) on_insert: Option, - pub(crate) on_replace: Option, - pub(crate) on_remove: Option, - pub(crate) on_despawn: Option, -} - -impl ComponentHooks { - pub(crate) fn update_from_component(&mut self) -> &mut Self { - if let Some(hook) = C::on_add() { - self.on_add(hook); - } - if let Some(hook) = C::on_insert() { - self.on_insert(hook); - } - if let Some(hook) = C::on_replace() { - self.on_replace(hook); - } - if let Some(hook) = C::on_remove() { - self.on_remove(hook); - } - if let Some(hook) = C::on_despawn() { - self.on_despawn(hook); - } - - self - } - - /// Register a [`ComponentHook`] that will be run when this component is added to an entity. - /// An `on_add` hook will always run before `on_insert` hooks. Spawning an entity counts as - /// adding all of its components. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_add` hook - pub fn on_add(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_add(hook) - .expect("Component already has an on_add hook") - } - - /// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`) - /// or replaced. - /// - /// An `on_insert` hook always runs after any `on_add` hooks (if the entity didn't already have the component). - /// - /// # Warning - /// - /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. - /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_insert` hook - pub fn on_insert(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_insert(hook) - .expect("Component already has an on_insert hook") - } - - /// Register a [`ComponentHook`] that will be run when this component is about to be dropped, - /// such as being replaced (with `.insert`) or removed. - /// - /// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced, - /// allowing access to the previous data just before it is dropped. - /// This hook does *not* run if the entity did not already have this component. - /// - /// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity). - /// - /// # Warning - /// - /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. - /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_replace` hook - pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_replace(hook) - .expect("Component already has an on_replace hook") - } - - /// Register a [`ComponentHook`] that will be run when this component is removed from an entity. - /// Despawning an entity counts as removing all of its components. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_remove` hook - pub fn on_remove(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_remove(hook) - .expect("Component already has an on_remove hook") - } - - /// Register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_despawn` hook - pub fn on_despawn(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_despawn(hook) - .expect("Component already has an on_despawn hook") - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is added to an entity. - /// - /// This is a fallible version of [`Self::on_add`]. - /// - /// Returns `None` if the component already has an `on_add` hook. - pub fn try_on_add(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_add.is_some() { - return None; - } - self.on_add = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is added (with `.insert`) - /// - /// This is a fallible version of [`Self::on_insert`]. - /// - /// Returns `None` if the component already has an `on_insert` hook. - pub fn try_on_insert(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_insert.is_some() { - return None; - } - self.on_insert = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed - /// - /// This is a fallible version of [`Self::on_replace`]. - /// - /// Returns `None` if the component already has an `on_replace` hook. - pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_replace.is_some() { - return None; - } - self.on_replace = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is removed from an entity. - /// - /// This is a fallible version of [`Self::on_remove`]. - /// - /// Returns `None` if the component already has an `on_remove` hook. - pub fn try_on_remove(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_remove.is_some() { - return None; - } - self.on_remove = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. - /// - /// This is a fallible version of [`Self::on_despawn`]. - /// - /// Returns `None` if the component already has an `on_despawn` hook. - pub fn try_on_despawn(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_despawn.is_some() { - return None; - } - self.on_despawn = Some(hook); - Some(self) - } -} - /// Stores metadata for a type of component or resource stored in a specific [`World`]. #[derive(Debug, Clone)] pub struct ComponentInfo { @@ -2052,7 +1817,7 @@ impl Components { } /// Gets the metadata associated with the given component, if it is registered. - /// This will return `None` if the id is not regiserted or is queued. + /// This will return `None` if the id is not registered or is queued. /// /// This will return an incorrect result if `id` did not come from the same world as `self`. It may return `None` or a garbage value. #[inline] @@ -2616,7 +2381,7 @@ impl Tick { /// /// Returns `true` if wrapping was performed. Otherwise, returns `false`. #[inline] - pub(crate) fn check_tick(&mut self, tick: Tick) -> bool { + pub fn check_tick(&mut self, tick: Tick) -> bool { let age = tick.relative_to(*self); // This comparison assumes that `age` has not overflowed `u32::MAX` before, which will be true // so long as this check always runs before that can happen. @@ -2629,6 +2394,41 @@ impl Tick { } } +/// An observer [`Event`] that can be used to maintain [`Tick`]s in custom data structures, enabling to make +/// use of bevy's periodic checks that clamps ticks to a certain range, preventing overflows and thus +/// keeping methods like [`Tick::is_newer_than`] reliably return `false` for ticks that got too old. +/// +/// # Example +/// +/// Here a schedule is stored in a custom resource. This way the systems in it would not have their change +/// ticks automatically updated via [`World::check_change_ticks`], possibly causing `Tick`-related bugs on +/// long-running apps. +/// +/// To fix that, add an observer for this event that calls the schedule's +/// [`Schedule::check_change_ticks`](bevy_ecs::schedule::Schedule::check_change_ticks). +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// use bevy_ecs::component::CheckChangeTicks; +/// +/// #[derive(Resource)] +/// struct CustomSchedule(Schedule); +/// +/// # let mut world = World::new(); +/// world.add_observer(|tick: Trigger, mut schedule: ResMut| { +/// schedule.0.check_change_ticks(tick.get()); +/// }); +/// ``` +#[derive(Debug, Clone, Copy, Event)] +pub struct CheckChangeTicks(pub(crate) Tick); + +impl CheckChangeTicks { + /// Get the `Tick` that can be used as the parameter of [`Tick::check_tick`]. + pub fn get(self) -> Tick { + self.0 + } +} + /// Interior-mutable access to the [`Tick`]s for a single component or resource. #[derive(Copy, Clone, Debug)] pub struct TickCells<'a> { diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index d525ba2e57..bb896c4f09 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -68,7 +68,7 @@ pub trait Event: Send + Sync + 'static { /// /// # Warning /// - /// This method should not be overridden by implementors, + /// This method should not be overridden by implementers, /// and should always correspond to the implementation of [`component_id`](Event::component_id). fn register_component_id(world: &mut World) -> ComponentId { world.register_component::>() @@ -82,7 +82,7 @@ pub trait Event: Send + Sync + 'static { /// /// # Warning /// - /// This method should not be overridden by implementors, + /// This method should not be overridden by implementers, /// and should always correspond to the implementation of [`register_component_id`](Event::register_component_id). fn component_id(world: &World) -> Option { world.component_id::>() diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index e138dfcac8..dfc32e60db 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -10,8 +10,9 @@ use crate::reflect::{ReflectComponent, ReflectFromWorld}; use crate::{ bundle::Bundle, - component::{Component, HookContext}, + component::Component, entity::Entity, + lifecycle::HookContext, relationship::{RelatedSpawner, RelatedSpawnerCommands}, system::EntityCommands, world::{DeferredWorld, EntityWorldMut, FromWorld, World}, diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 14802dfc8e..7c10127fcc 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -41,6 +41,7 @@ pub mod event; pub mod hierarchy; pub mod intern; pub mod label; +pub mod lifecycle; pub mod name; pub mod never; pub mod observer; @@ -48,7 +49,6 @@ pub mod query; #[cfg(feature = "bevy_reflect")] pub mod reflect; pub mod relationship; -pub mod removal_detection; pub mod resource; pub mod schedule; pub mod spawn; @@ -76,12 +76,12 @@ pub mod prelude { error::{BevyError, Result}, event::{Event, EventMutator, EventReader, EventWriter, Events}, hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children}, + lifecycle::{OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace, RemovedComponents}, name::{Name, NameOrEntity}, observer::{Observer, Trigger}, query::{Added, Allows, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, related, relationship::RelationshipTarget, - removal_detection::RemovedComponents, resource::Resource, schedule::{ common_conditions::*, ApplyDeferred, IntoScheduleConfigs, IntoSystemSet, Schedule, @@ -96,7 +96,7 @@ pub mod prelude { }, world::{ EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, - FromWorld, OnAdd, OnInsert, OnRemove, OnReplace, World, + FromWorld, World, }, }; diff --git a/crates/bevy_ecs/src/lifecycle.rs b/crates/bevy_ecs/src/lifecycle.rs new file mode 100644 index 0000000000..e228b416c3 --- /dev/null +++ b/crates/bevy_ecs/src/lifecycle.rs @@ -0,0 +1,606 @@ +//! This module contains various tools to allow you to react to component insertion or removal, +//! as well as entity spawning and despawning. +//! +//! There are four main ways to react to these lifecycle events: +//! +//! 1. Using component hooks, which act as inherent constructors and destructors for components. +//! 2. Using [observers], which are a user-extensible way to respond to events, including component lifecycle events. +//! 3. Using the [`RemovedComponents`] system parameter, which offers an event-style interface. +//! 4. Using the [`Added`] query filter, which checks each component to see if it has been added since the last time a system ran. +//! +//! [observers]: crate::observer +//! [`Added`]: crate::query::Added +//! +//! # Types of lifecycle events +//! +//! There are five types of lifecycle events, split into two categories. First, we have lifecycle events that are triggered +//! when a component is added to an entity: +//! +//! - [`OnAdd`]: Triggered when a component is added to an entity that did not already have it. +//! - [`OnInsert`]: Triggered when a component is added to an entity, regardless of whether it already had it. +//! +//! When both events occur, [`OnAdd`] hooks are evaluated before [`OnInsert`]. +//! +//! Next, we have lifecycle events that are triggered when a component is removed from an entity: +//! +//! - [`OnReplace`]: Triggered when a component is removed from an entity, regardless if it is then replaced with a new value. +//! - [`OnRemove`]: Triggered when a component is removed from an entity and not replaced, before the component is removed. +//! - [`OnDespawn`]: Triggered for each component on an entity when it is despawned. +//! +//! [`OnReplace`] hooks are evaluated before [`OnRemove`], then finally [`OnDespawn`] hooks are evaluated. +//! +//! [`OnAdd`] and [`OnRemove`] are counterparts: they are only triggered when a component is added or removed +//! from an entity in such a way as to cause a change in the component's presence on that entity. +//! Similarly, [`OnInsert`] and [`OnReplace`] are counterparts: they are triggered when a component is added or replaced +//! on an entity, regardless of whether this results in a change in the component's presence on that entity. +//! +//! To reliably synchronize data structures using with component lifecycle events, +//! you can combine [`OnInsert`] and [`OnReplace`] to fully capture any changes to the data. +//! This is particularly useful in combination with immutable components, +//! to avoid any lifecycle-bypassing mutations. +//! +//! ## Lifecycle events and component types +//! +//! Despite the absence of generics, each lifecycle event is associated with a specific component. +//! When defining a component hook for a [`Component`] type, that component is used. +//! When listening to lifecycle events for observers, the `B: Bundle` generic is used. +//! +//! Each of these lifecycle events also corresponds to a fixed [`ComponentId`], +//! which are assigned during [`World`] initialization. +//! For example, [`OnAdd`] corresponds to [`ON_ADD`]. +//! This is used to skip [`TypeId`](core::any::TypeId) lookups in hot paths. +use crate::{ + change_detection::MaybeLocation, + component::{Component, ComponentId, ComponentIdFor, Tick}, + entity::Entity, + event::{Event, EventCursor, EventId, EventIterator, EventIteratorWithId, Events}, + relationship::RelationshipHookMode, + storage::SparseSet, + system::{Local, ReadOnlySystemParam, SystemMeta, SystemParam}, + world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, +}; + +use derive_more::derive::Into; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; +use core::{ + fmt::Debug, + iter, + marker::PhantomData, + ops::{Deref, DerefMut}, + option, +}; + +/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove`. +pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, HookContext); + +/// Context provided to a [`ComponentHook`]. +#[derive(Clone, Copy, Debug)] +pub struct HookContext { + /// The [`Entity`] this hook was invoked for. + pub entity: Entity, + /// The [`ComponentId`] this hook was invoked for. + pub component_id: ComponentId, + /// The caller location is `Some` if the `track_caller` feature is enabled. + pub caller: MaybeLocation, + /// Configures how relationship hooks will run + pub relationship_hook_mode: RelationshipHookMode, +} + +/// [`World`]-mutating functions that run as part of lifecycle events of a [`Component`]. +/// +/// Hooks are functions that run when a component is added, overwritten, or removed from an entity. +/// These are intended to be used for structural side effects that need to happen when a component is added or removed, +/// and are not intended for general-purpose logic. +/// +/// For example, you might use a hook to update a cached index when a component is added, +/// to clean up resources when a component is removed, +/// or to keep hierarchical data structures across entities in sync. +/// +/// This information is stored in the [`ComponentInfo`](crate::component::ComponentInfo) of the associated component. +/// +/// There are two ways of configuring hooks for a component: +/// 1. Defining the relevant hooks on the [`Component`] implementation +/// 2. Using the [`World::register_component_hooks`] method +/// +/// # Example +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// use bevy_platform::collections::HashSet; +/// +/// #[derive(Component)] +/// struct MyTrackedComponent; +/// +/// #[derive(Resource, Default)] +/// struct TrackedEntities(HashSet); +/// +/// let mut world = World::new(); +/// world.init_resource::(); +/// +/// // No entities with `MyTrackedComponent` have been added yet, so we can safely add component hooks +/// let mut tracked_component_query = world.query::<&MyTrackedComponent>(); +/// assert!(tracked_component_query.iter(&world).next().is_none()); +/// +/// world.register_component_hooks::().on_add(|mut world, context| { +/// let mut tracked_entities = world.resource_mut::(); +/// tracked_entities.0.insert(context.entity); +/// }); +/// +/// world.register_component_hooks::().on_remove(|mut world, context| { +/// let mut tracked_entities = world.resource_mut::(); +/// tracked_entities.0.remove(&context.entity); +/// }); +/// +/// let entity = world.spawn(MyTrackedComponent).id(); +/// let tracked_entities = world.resource::(); +/// assert!(tracked_entities.0.contains(&entity)); +/// +/// world.despawn(entity); +/// let tracked_entities = world.resource::(); +/// assert!(!tracked_entities.0.contains(&entity)); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct ComponentHooks { + pub(crate) on_add: Option, + pub(crate) on_insert: Option, + pub(crate) on_replace: Option, + pub(crate) on_remove: Option, + pub(crate) on_despawn: Option, +} + +impl ComponentHooks { + pub(crate) fn update_from_component(&mut self) -> &mut Self { + if let Some(hook) = C::on_add() { + self.on_add(hook); + } + if let Some(hook) = C::on_insert() { + self.on_insert(hook); + } + if let Some(hook) = C::on_replace() { + self.on_replace(hook); + } + if let Some(hook) = C::on_remove() { + self.on_remove(hook); + } + if let Some(hook) = C::on_despawn() { + self.on_despawn(hook); + } + + self + } + + /// Register a [`ComponentHook`] that will be run when this component is added to an entity. + /// An `on_add` hook will always run before `on_insert` hooks. Spawning an entity counts as + /// adding all of its components. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_add` hook + pub fn on_add(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_add(hook) + .expect("Component already has an on_add hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`) + /// or replaced. + /// + /// An `on_insert` hook always runs after any `on_add` hooks (if the entity didn't already have the component). + /// + /// # Warning + /// + /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. + /// As a result, this needs to be combined with immutable components to serve as a mechanism for reliably updating indexes and other caches. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_insert` hook + pub fn on_insert(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_insert(hook) + .expect("Component already has an on_insert hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is about to be dropped, + /// such as being replaced (with `.insert`) or removed. + /// + /// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced, + /// allowing access to the previous data just before it is dropped. + /// This hook does *not* run if the entity did not already have this component. + /// + /// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity). + /// + /// # Warning + /// + /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. + /// As a result, this needs to be combined with immutable components to serve as a mechanism for reliably updating indexes and other caches. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_replace` hook + pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_replace(hook) + .expect("Component already has an on_replace hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is removed from an entity. + /// Despawning an entity counts as removing all of its components. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_remove` hook + pub fn on_remove(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_remove(hook) + .expect("Component already has an on_remove hook") + } + + /// Register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_despawn` hook + pub fn on_despawn(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_despawn(hook) + .expect("Component already has an on_despawn hook") + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is added to an entity. + /// + /// This is a fallible version of [`Self::on_add`]. + /// + /// Returns `None` if the component already has an `on_add` hook. + pub fn try_on_add(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_add.is_some() { + return None; + } + self.on_add = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is added (with `.insert`) + /// + /// This is a fallible version of [`Self::on_insert`]. + /// + /// Returns `None` if the component already has an `on_insert` hook. + pub fn try_on_insert(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_insert.is_some() { + return None; + } + self.on_insert = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed + /// + /// This is a fallible version of [`Self::on_replace`]. + /// + /// Returns `None` if the component already has an `on_replace` hook. + pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_replace.is_some() { + return None; + } + self.on_replace = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is removed from an entity. + /// + /// This is a fallible version of [`Self::on_remove`]. + /// + /// Returns `None` if the component already has an `on_remove` hook. + pub fn try_on_remove(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_remove.is_some() { + return None; + } + self.on_remove = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. + /// + /// This is a fallible version of [`Self::on_despawn`]. + /// + /// Returns `None` if the component already has an `on_despawn` hook. + pub fn try_on_despawn(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_despawn.is_some() { + return None; + } + self.on_despawn = Some(hook); + Some(self) + } +} + +/// [`ComponentId`] for [`OnAdd`] +pub const ON_ADD: ComponentId = ComponentId::new(0); +/// [`ComponentId`] for [`OnInsert`] +pub const ON_INSERT: ComponentId = ComponentId::new(1); +/// [`ComponentId`] for [`OnReplace`] +pub const ON_REPLACE: ComponentId = ComponentId::new(2); +/// [`ComponentId`] for [`OnRemove`] +pub const ON_REMOVE: ComponentId = ComponentId::new(3); +/// [`ComponentId`] for [`OnDespawn`] +pub const ON_DESPAWN: ComponentId = ComponentId::new(4); + +/// Trigger emitted when a component is inserted onto an entity that does not already have that +/// component. Runs before `OnInsert`. +/// See [`crate::lifecycle::ComponentHooks::on_add`] for more information. +#[derive(Event, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +pub struct OnAdd; + +/// Trigger emitted when a component is inserted, regardless of whether or not the entity already +/// had that component. Runs after `OnAdd`, if it ran. +/// See [`crate::lifecycle::ComponentHooks::on_insert`] for more information. +#[derive(Event, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +pub struct OnInsert; + +/// Trigger emitted when a component is removed from an entity, regardless +/// of whether or not it is later replaced. +/// +/// Runs before the value is replaced, so you can still access the original component data. +/// See [`crate::lifecycle::ComponentHooks::on_replace`] for more information. +#[derive(Event, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +pub struct OnReplace; + +/// Trigger emitted when a component is removed from an entity, and runs before the component is +/// removed, so you can still access the component data. +/// See [`crate::lifecycle::ComponentHooks::on_remove`] for more information. +#[derive(Event, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +pub struct OnRemove; + +/// Trigger emitted for each component on an entity when it is despawned. +/// See [`crate::lifecycle::ComponentHooks::on_despawn`] for more information. +#[derive(Event, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +pub struct OnDespawn; + +/// Wrapper around [`Entity`] for [`RemovedComponents`]. +/// Internally, `RemovedComponents` uses these as an `Events`. +#[derive(Event, Debug, Clone, Into)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))] +pub struct RemovedComponentEntity(Entity); + +/// Wrapper around a [`EventCursor`] so that we +/// can differentiate events between components. +#[derive(Debug)] +pub struct RemovedComponentReader +where + T: Component, +{ + reader: EventCursor, + marker: PhantomData, +} + +impl Default for RemovedComponentReader { + fn default() -> Self { + Self { + reader: Default::default(), + marker: PhantomData, + } + } +} + +impl Deref for RemovedComponentReader { + type Target = EventCursor; + fn deref(&self) -> &Self::Target { + &self.reader + } +} + +impl DerefMut for RemovedComponentReader { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.reader + } +} + +/// Stores the [`RemovedComponents`] event buffers for all types of component in a given [`World`]. +#[derive(Default, Debug)] +pub struct RemovedComponentEvents { + event_sets: SparseSet>, +} + +impl RemovedComponentEvents { + /// Creates an empty storage buffer for component removal events. + pub fn new() -> Self { + Self::default() + } + + /// For each type of component, swaps the event buffers and clears the oldest event buffer. + /// In general, this should be called once per frame/update. + pub fn update(&mut self) { + for (_component_id, events) in self.event_sets.iter_mut() { + events.update(); + } + } + + /// Returns an iterator over components and their entity events. + pub fn iter(&self) -> impl Iterator)> { + self.event_sets.iter() + } + + /// Gets the event storage for a given component. + pub fn get( + &self, + component_id: impl Into, + ) -> Option<&Events> { + self.event_sets.get(component_id.into()) + } + + /// Sends a removal event for the specified component. + pub fn send(&mut self, component_id: impl Into, entity: Entity) { + self.event_sets + .get_or_insert_with(component_id.into(), Default::default) + .send(RemovedComponentEntity(entity)); + } +} + +/// A [`SystemParam`] that yields entities that had their `T` [`Component`] +/// removed or have been despawned with it. +/// +/// This acts effectively the same as an [`EventReader`](crate::event::EventReader). +/// +/// Unlike hooks or observers (see the [lifecycle](crate) module docs), +/// this does not allow you to see which data existed before removal. +/// +/// If you are using `bevy_ecs` as a standalone crate, +/// note that the [`RemovedComponents`] list will not be automatically cleared for you, +/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers). +/// +/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is +/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`. +/// For the main world, this is delayed until after all `SubApp`s have run. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::system::IntoSystem; +/// # use bevy_ecs::lifecycle::RemovedComponents; +/// # +/// # #[derive(Component)] +/// # struct MyComponent; +/// fn react_on_removal(mut removed: RemovedComponents) { +/// removed.read().for_each(|removed_entity| println!("{}", removed_entity)); +/// } +/// # bevy_ecs::system::assert_is_system(react_on_removal); +/// ``` +#[derive(SystemParam)] +pub struct RemovedComponents<'w, 's, T: Component> { + component_id: ComponentIdFor<'s, T>, + reader: Local<'s, RemovedComponentReader>, + event_sets: &'w RemovedComponentEvents, +} + +/// Iterator over entities that had a specific component removed. +/// +/// See [`RemovedComponents`]. +pub type RemovedIter<'a> = iter::Map< + iter::Flatten>>>, + fn(RemovedComponentEntity) -> Entity, +>; + +/// Iterator over entities that had a specific component removed. +/// +/// See [`RemovedComponents`]. +pub type RemovedIterWithId<'a> = iter::Map< + iter::Flatten>>, + fn( + (&RemovedComponentEntity, EventId), + ) -> (Entity, EventId), +>; + +fn map_id_events( + (entity, id): (&RemovedComponentEntity, EventId), +) -> (Entity, EventId) { + (entity.clone().into(), id) +} + +// For all practical purposes, the api surface of `RemovedComponents` +// should be similar to `EventReader` to reduce confusion. +impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> { + /// Fetch underlying [`EventCursor`]. + pub fn reader(&self) -> &EventCursor { + &self.reader + } + + /// Fetch underlying [`EventCursor`] mutably. + pub fn reader_mut(&mut self) -> &mut EventCursor { + &mut self.reader + } + + /// Fetch underlying [`Events`]. + pub fn events(&self) -> Option<&Events> { + self.event_sets.get(self.component_id.get()) + } + + /// Destructures to get a mutable reference to the `EventCursor` + /// and a reference to `Events`. + /// + /// This is necessary since Rust can't detect destructuring through methods and most + /// usecases of the reader uses the `Events` as well. + pub fn reader_mut_with_events( + &mut self, + ) -> Option<( + &mut RemovedComponentReader, + &Events, + )> { + self.event_sets + .get(self.component_id.get()) + .map(|events| (&mut *self.reader, events)) + } + + /// Iterates over the events this [`RemovedComponents`] has not seen yet. This updates the + /// [`RemovedComponents`]'s event counter, which means subsequent event reads will not include events + /// that happened before now. + pub fn read(&mut self) -> RemovedIter<'_> { + self.reader_mut_with_events() + .map(|(reader, events)| reader.read(events).cloned()) + .into_iter() + .flatten() + .map(RemovedComponentEntity::into) + } + + /// Like [`read`](Self::read), except also returning the [`EventId`] of the events. + pub fn read_with_id(&mut self) -> RemovedIterWithId<'_> { + self.reader_mut_with_events() + .map(|(reader, events)| reader.read_with_id(events)) + .into_iter() + .flatten() + .map(map_id_events) + } + + /// Determines the number of removal events available to be read from this [`RemovedComponents`] without consuming any. + pub fn len(&self) -> usize { + self.events() + .map(|events| self.reader.len(events)) + .unwrap_or(0) + } + + /// Returns `true` if there are no events available to read. + pub fn is_empty(&self) -> bool { + self.events() + .is_none_or(|events| self.reader.is_empty(events)) + } + + /// Consumes all available events. + /// + /// This means these events will not appear in calls to [`RemovedComponents::read()`] or + /// [`RemovedComponents::read_with_id()`] and [`RemovedComponents::is_empty()`] will return `true`. + pub fn clear(&mut self) { + if let Some((reader, events)) = self.reader_mut_with_events() { + reader.clear(events); + } + } +} + +// SAFETY: Only reads World removed component events +unsafe impl<'a> ReadOnlySystemParam for &'a RemovedComponentEvents {} + +// SAFETY: no component value access. +unsafe impl<'a> SystemParam for &'a RemovedComponentEvents { + type State = (); + type Item<'w, 's> = &'w RemovedComponentEvents; + + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + + #[inline] + unsafe fn get_param<'w, 's>( + _state: &'s mut Self::State, + _system_meta: &SystemMeta, + world: UnsafeWorldCell<'w>, + _change_tick: Tick, + ) -> Self::Item<'w, 's> { + world.removed_components() + } +} diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_observer.rs index 2c2d42b1c9..bd45072a5a 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_observer.rs @@ -1,8 +1,7 @@ use crate::{ - component::{ - Component, ComponentCloneBehavior, ComponentHook, HookContext, Mutable, StorageType, - }, + component::{Component, ComponentCloneBehavior, Mutable, StorageType}, entity::{ComponentCloneCtx, Entity, EntityClonerBuilder, EntityMapper, SourceComponent}, + lifecycle::{ComponentHook, HookContext}, world::World, }; use alloc::vec::Vec; diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index ed5a8b176f..6fcfa1621c 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -391,6 +391,8 @@ pub struct Observers { impl Observers { pub(crate) fn get_observers(&mut self, event_type: ComponentId) -> &mut CachedObservers { + use crate::lifecycle::*; + match event_type { ON_ADD => &mut self.on_add, ON_INSERT => &mut self.on_insert, @@ -402,6 +404,8 @@ impl Observers { } pub(crate) fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { + use crate::lifecycle::*; + match event_type { ON_ADD => Some(&self.on_add), ON_INSERT => Some(&self.on_insert), @@ -479,6 +483,8 @@ impl Observers { } pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option { + use crate::lifecycle::*; + match event_type { ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index c3acff2e6e..61cc0973f3 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -2,8 +2,9 @@ use alloc::{boxed::Box, vec}; use core::any::Any; use crate::{ - component::{ComponentHook, ComponentId, HookContext, Mutable, StorageType}, + component::{ComponentId, Mutable, StorageType}, error::{ErrorContext, ErrorHandler}, + lifecycle::{ComponentHook, HookContext}, observer::{ObserverDescriptor, ObserverTrigger}, prelude::*, query::DebugCheckedUnwrap, @@ -410,7 +411,7 @@ fn observer_system_runner>( } } -/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::component::ComponentHooks::on_add`). +/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::lifecycle::ComponentHooks::on_add`). /// /// This function exists separate from [`Observer`] to allow [`Observer`] to have its type parameters /// erased. diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index 9ec67ce36a..00334cb6d0 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -11,9 +11,10 @@ pub use relationship_query::*; pub use relationship_source_collection::*; use crate::{ - component::{Component, HookContext, Mutable}, + component::{Component, Mutable}, entity::{ComponentCloneCtx, Entity, SourceComponent}, error::{ignore, CommandWithEntity, HandleError}, + lifecycle::HookContext, world::{DeferredWorld, EntityWorldMut}, }; use log::warn; diff --git a/crates/bevy_ecs/src/removal_detection.rs b/crates/bevy_ecs/src/removal_detection.rs deleted file mode 100644 index 64cc63a7ce..0000000000 --- a/crates/bevy_ecs/src/removal_detection.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! Alerting events when a component is removed from an entity. - -use crate::{ - component::{Component, ComponentId, ComponentIdFor, Tick}, - entity::Entity, - event::{Event, EventCursor, EventId, EventIterator, EventIteratorWithId, Events}, - prelude::Local, - storage::SparseSet, - system::{ReadOnlySystemParam, SystemMeta, SystemParam}, - world::{unsafe_world_cell::UnsafeWorldCell, World}, -}; - -use derive_more::derive::Into; - -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::Reflect; -use core::{ - fmt::Debug, - iter, - marker::PhantomData, - ops::{Deref, DerefMut}, - option, -}; - -/// Wrapper around [`Entity`] for [`RemovedComponents`]. -/// Internally, `RemovedComponents` uses these as an `Events`. -#[derive(Event, Debug, Clone, Into)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))] -pub struct RemovedComponentEntity(Entity); - -/// Wrapper around a [`EventCursor`] so that we -/// can differentiate events between components. -#[derive(Debug)] -pub struct RemovedComponentReader -where - T: Component, -{ - reader: EventCursor, - marker: PhantomData, -} - -impl Default for RemovedComponentReader { - fn default() -> Self { - Self { - reader: Default::default(), - marker: PhantomData, - } - } -} - -impl Deref for RemovedComponentReader { - type Target = EventCursor; - fn deref(&self) -> &Self::Target { - &self.reader - } -} - -impl DerefMut for RemovedComponentReader { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.reader - } -} - -/// Stores the [`RemovedComponents`] event buffers for all types of component in a given [`World`]. -#[derive(Default, Debug)] -pub struct RemovedComponentEvents { - event_sets: SparseSet>, -} - -impl RemovedComponentEvents { - /// Creates an empty storage buffer for component removal events. - pub fn new() -> Self { - Self::default() - } - - /// For each type of component, swaps the event buffers and clears the oldest event buffer. - /// In general, this should be called once per frame/update. - pub fn update(&mut self) { - for (_component_id, events) in self.event_sets.iter_mut() { - events.update(); - } - } - - /// Returns an iterator over components and their entity events. - pub fn iter(&self) -> impl Iterator)> { - self.event_sets.iter() - } - - /// Gets the event storage for a given component. - pub fn get( - &self, - component_id: impl Into, - ) -> Option<&Events> { - self.event_sets.get(component_id.into()) - } - - /// Sends a removal event for the specified component. - pub fn send(&mut self, component_id: impl Into, entity: Entity) { - self.event_sets - .get_or_insert_with(component_id.into(), Default::default) - .send(RemovedComponentEntity(entity)); - } -} - -/// A [`SystemParam`] that yields entities that had their `T` [`Component`] -/// removed or have been despawned with it. -/// -/// This acts effectively the same as an [`EventReader`](crate::event::EventReader). -/// -/// Note that this does not allow you to see which data existed before removal. -/// If you need this, you will need to track the component data value on your own, -/// using a regularly scheduled system that requests `Query<(Entity, &T), Changed>` -/// and stores the data somewhere safe to later cross-reference. -/// -/// If you are using `bevy_ecs` as a standalone crate, -/// note that the `RemovedComponents` list will not be automatically cleared for you, -/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers). -/// -/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is -/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`. -/// For the main world, this is delayed until after all `SubApp`s have run. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ``` -/// # use bevy_ecs::component::Component; -/// # use bevy_ecs::system::IntoSystem; -/// # use bevy_ecs::removal_detection::RemovedComponents; -/// # -/// # #[derive(Component)] -/// # struct MyComponent; -/// fn react_on_removal(mut removed: RemovedComponents) { -/// removed.read().for_each(|removed_entity| println!("{}", removed_entity)); -/// } -/// # bevy_ecs::system::assert_is_system(react_on_removal); -/// ``` -#[derive(SystemParam)] -pub struct RemovedComponents<'w, 's, T: Component> { - component_id: ComponentIdFor<'s, T>, - reader: Local<'s, RemovedComponentReader>, - event_sets: &'w RemovedComponentEvents, -} - -/// Iterator over entities that had a specific component removed. -/// -/// See [`RemovedComponents`]. -pub type RemovedIter<'a> = iter::Map< - iter::Flatten>>>, - fn(RemovedComponentEntity) -> Entity, ->; - -/// Iterator over entities that had a specific component removed. -/// -/// See [`RemovedComponents`]. -pub type RemovedIterWithId<'a> = iter::Map< - iter::Flatten>>, - fn( - (&RemovedComponentEntity, EventId), - ) -> (Entity, EventId), ->; - -fn map_id_events( - (entity, id): (&RemovedComponentEntity, EventId), -) -> (Entity, EventId) { - (entity.clone().into(), id) -} - -// For all practical purposes, the api surface of `RemovedComponents` -// should be similar to `EventReader` to reduce confusion. -impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> { - /// Fetch underlying [`EventCursor`]. - pub fn reader(&self) -> &EventCursor { - &self.reader - } - - /// Fetch underlying [`EventCursor`] mutably. - pub fn reader_mut(&mut self) -> &mut EventCursor { - &mut self.reader - } - - /// Fetch underlying [`Events`]. - pub fn events(&self) -> Option<&Events> { - self.event_sets.get(self.component_id.get()) - } - - /// Destructures to get a mutable reference to the `EventCursor` - /// and a reference to `Events`. - /// - /// This is necessary since Rust can't detect destructuring through methods and most - /// usecases of the reader uses the `Events` as well. - pub fn reader_mut_with_events( - &mut self, - ) -> Option<( - &mut RemovedComponentReader, - &Events, - )> { - self.event_sets - .get(self.component_id.get()) - .map(|events| (&mut *self.reader, events)) - } - - /// Iterates over the events this [`RemovedComponents`] has not seen yet. This updates the - /// [`RemovedComponents`]'s event counter, which means subsequent event reads will not include events - /// that happened before now. - pub fn read(&mut self) -> RemovedIter<'_> { - self.reader_mut_with_events() - .map(|(reader, events)| reader.read(events).cloned()) - .into_iter() - .flatten() - .map(RemovedComponentEntity::into) - } - - /// Like [`read`](Self::read), except also returning the [`EventId`] of the events. - pub fn read_with_id(&mut self) -> RemovedIterWithId<'_> { - self.reader_mut_with_events() - .map(|(reader, events)| reader.read_with_id(events)) - .into_iter() - .flatten() - .map(map_id_events) - } - - /// Determines the number of removal events available to be read from this [`RemovedComponents`] without consuming any. - pub fn len(&self) -> usize { - self.events() - .map(|events| self.reader.len(events)) - .unwrap_or(0) - } - - /// Returns `true` if there are no events available to read. - pub fn is_empty(&self) -> bool { - self.events() - .is_none_or(|events| self.reader.is_empty(events)) - } - - /// Consumes all available events. - /// - /// This means these events will not appear in calls to [`RemovedComponents::read()`] or - /// [`RemovedComponents::read_with_id()`] and [`RemovedComponents::is_empty()`] will return `true`. - pub fn clear(&mut self) { - if let Some((reader, events)) = self.reader_mut_with_events() { - reader.clear(events); - } - } -} - -// SAFETY: Only reads World removed component events -unsafe impl<'a> ReadOnlySystemParam for &'a RemovedComponentEvents {} - -// SAFETY: no component value access. -unsafe impl<'a> SystemParam for &'a RemovedComponentEvents { - type State = (); - type Item<'w, 's> = &'w RemovedComponentEvents; - - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} - - #[inline] - unsafe fn get_param<'w, 's>( - _state: &'s mut Self::State, - _system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - _change_tick: Tick, - ) -> Self::Item<'w, 's> { - world.removed_components() - } -} diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 2b31ad50c7..45eb4febb2 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -11,8 +11,18 @@ pub type BoxedCondition = Box>; /// A system that determines if one or more scheduled systems should run. /// -/// Implemented for functions and closures that convert into [`System`](System) -/// with [read-only](crate::system::ReadOnlySystemParam) parameters. +/// `SystemCondition` is sealed and implemented for functions and closures with +/// [read-only](crate::system::ReadOnlySystemParam) parameters that convert into +/// [`System`](System), [`System>`](System) or +/// [`System>`](System). +/// +/// `SystemCondition` offers a private method +/// (called by [`run_if`](crate::schedule::IntoScheduleConfigs::run_if) and the provided methods) +/// that converts the implementing system into a condition (system) returning a bool. +/// Depending on the output type of the implementing system: +/// - `bool`: the implementing system is used as the condition; +/// - `Result<(), BevyError>`: the condition returns `true` if and only if the implementing system returns `Ok(())`; +/// - `Result`: the condition returns `true` if and only if the implementing system returns `Ok(true)`. /// /// # Marker type parameter /// @@ -31,7 +41,7 @@ pub type BoxedCondition = Box>; /// ``` /// /// # Examples -/// A condition that returns true every other time it's called. +/// A condition that returns `true` every other time it's called. /// ``` /// # use bevy_ecs::prelude::*; /// fn every_other_time() -> impl SystemCondition<()> { @@ -54,7 +64,7 @@ pub type BoxedCondition = Box>; /// # assert!(!world.resource::().0); /// ``` /// -/// A condition that takes a bool as an input and returns it unchanged. +/// A condition that takes a `bool` as an input and returns it unchanged. /// /// ``` /// # use bevy_ecs::prelude::*; @@ -71,8 +81,30 @@ pub type BoxedCondition = Box>; /// # world.insert_resource(DidRun(false)); /// # app.run(&mut world); /// # assert!(world.resource::().0); -pub trait SystemCondition: - sealed::SystemCondition +/// ``` +/// +/// A condition returning a `Result<(), BevyError>` +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # #[derive(Component)] struct Player; +/// fn player_exists(q_player: Query<(), With>) -> Result { +/// Ok(q_player.single()?) +/// } +/// +/// # let mut app = Schedule::default(); +/// # #[derive(Resource)] struct DidRun(bool); +/// # fn my_system(mut did_run: ResMut) { did_run.0 = true; } +/// app.add_systems(my_system.run_if(player_exists)); +/// # let mut world = World::new(); +/// # world.insert_resource(DidRun(false)); +/// # app.run(&mut world); +/// # assert!(!world.resource::().0); +/// # world.spawn(Player); +/// # app.run(&mut world); +/// # assert!(world.resource::().0); +pub trait SystemCondition: + sealed::SystemCondition { /// Returns a new run condition that only returns `true` /// if both this one and the passed `and` return `true`. @@ -371,28 +403,61 @@ pub trait SystemCondition: } } -impl SystemCondition for F where - F: sealed::SystemCondition +impl SystemCondition for F where + F: sealed::SystemCondition { } mod sealed { - use crate::system::{IntoSystem, ReadOnlySystem, SystemInput}; + use crate::{ + error::BevyError, + system::{IntoSystem, ReadOnlySystem, SystemInput}, + }; - pub trait SystemCondition: - IntoSystem + pub trait SystemCondition: + IntoSystem { // This associated type is necessary to let the compiler // know that `Self::System` is `ReadOnlySystem`. - type ReadOnlySystem: ReadOnlySystem; + type ReadOnlySystem: ReadOnlySystem; + + fn into_condition_system(self) -> impl ReadOnlySystem; } - impl SystemCondition for F + impl SystemCondition for F where F: IntoSystem, F::System: ReadOnlySystem, { type ReadOnlySystem = F::System; + + fn into_condition_system(self) -> impl ReadOnlySystem { + IntoSystem::into_system(self) + } + } + + impl SystemCondition> for F + where + F: IntoSystem, Marker>, + F::System: ReadOnlySystem, + { + type ReadOnlySystem = F::System; + + fn into_condition_system(self) -> impl ReadOnlySystem { + IntoSystem::into_system(self.map(|result| result.is_ok())) + } + } + + impl SystemCondition> for F + where + F: IntoSystem, Marker>, + F::System: ReadOnlySystem, + { + type ReadOnlySystem = F::System; + + fn into_condition_system(self) -> impl ReadOnlySystem { + IntoSystem::into_system(self.map(|result| matches!(result, Ok(true)))) + } } } @@ -402,9 +467,9 @@ pub mod common_conditions { use crate::{ change_detection::DetectChanges, event::{Event, EventReader}, + lifecycle::RemovedComponents, prelude::{Component, Query, With}, query::QueryFilter, - removal_detection::RemovedComponents, resource::Resource, system::{In, IntoSystem, Local, Res, System, SystemInput}, }; diff --git a/crates/bevy_ecs/src/schedule/config.rs b/crates/bevy_ecs/src/schedule/config.rs index f1a48e432b..4826d0a66d 100644 --- a/crates/bevy_ecs/src/schedule/config.rs +++ b/crates/bevy_ecs/src/schedule/config.rs @@ -14,8 +14,8 @@ use crate::{ system::{BoxedSystem, InfallibleSystemWrapper, IntoSystem, ScheduleSystem, System}, }; -fn new_condition(condition: impl SystemCondition) -> BoxedCondition { - let condition_system = IntoSystem::into_system(condition); +fn new_condition(condition: impl SystemCondition) -> BoxedCondition { + let condition_system = condition.into_condition_system(); assert!( condition_system.is_send(), "SystemCondition `{}` accesses `NonSend` resources. This is not currently supported.", @@ -447,7 +447,7 @@ pub trait IntoScheduleConfigs(self, condition: impl SystemCondition) -> ScheduleConfigs { + fn run_if(self, condition: impl SystemCondition) -> ScheduleConfigs { self.into_configs().run_if(condition) } @@ -535,7 +535,7 @@ impl> IntoScheduleCo self } - fn run_if(mut self, condition: impl SystemCondition) -> ScheduleConfigs { + fn run_if(mut self, condition: impl SystemCondition) -> ScheduleConfigs { self.run_if_dyn(new_condition(condition)); self } diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index b73ffdfd33..38b85c1ca5 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -18,7 +18,7 @@ use crate::{ component::{ComponentId, Tick}, error::{BevyError, ErrorContext, Result}, prelude::{IntoSystemSet, SystemSet}, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet}, system::{ScheduleSystem, System, SystemIn, SystemParamValidationError, SystemStateFlags}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, @@ -162,12 +162,8 @@ impl System for ApplyDeferred { Cow::Borrowed("bevy_ecs::apply_deferred") } - fn component_access(&self) -> &Access { - // This system accesses no components. - const { &Access::new() } - } - fn component_access_set(&self) -> &FilteredAccessSet { + // This system accesses no components. const { &FilteredAccessSet::new() } } diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 807112f438..8c5aa1d6fb 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -29,6 +29,7 @@ mod tests { use alloc::{string::ToString, vec, vec::Vec}; use core::sync::atomic::{AtomicU32, Ordering}; + use crate::error::BevyError; pub use crate::{ prelude::World, resource::Resource, @@ -49,10 +50,10 @@ mod tests { struct SystemOrder(Vec); #[derive(Resource, Default)] - struct RunConditionBool(pub bool); + struct RunConditionBool(bool); #[derive(Resource, Default)] - struct Counter(pub AtomicU32); + struct Counter(AtomicU32); fn make_exclusive_system(tag: u32) -> impl FnMut(&mut World) { move |world| world.resource_mut::().0.push(tag) @@ -252,12 +253,13 @@ mod tests { } mod conditions { + use crate::change_detection::DetectChanges; use super::*; #[test] - fn system_with_condition() { + fn system_with_condition_bool() { let mut world = World::default(); let mut schedule = Schedule::default(); @@ -276,6 +278,47 @@ mod tests { assert_eq!(world.resource::().0, vec![0]); } + #[test] + fn system_with_condition_result_unit() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_systems( + make_function_system(0).run_if(|| Err::<(), BevyError>(core::fmt::Error.into())), + ); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![]); + + schedule.add_systems(make_function_system(1).run_if(|| Ok(()))); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![1]); + } + + #[test] + fn system_with_condition_result_bool() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_systems(( + make_function_system(0).run_if(|| Err::(core::fmt::Error.into())), + make_function_system(1).run_if(|| Ok(false)), + )); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![]); + + schedule.add_systems(make_function_system(2).run_if(|| Ok(true))); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![2]); + } + #[test] fn systems_with_distributive_condition() { let mut world = World::default(); diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 2d559db1dd..c4e61356d0 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -166,7 +166,7 @@ impl Schedules { writeln!(message, "{}", components.get_name(*id).unwrap()).unwrap(); } - info!("{}", message); + info!("{message}"); } /// Adds one or more systems to the [`Schedule`] matching the provided [`ScheduleLabel`]. @@ -558,7 +558,7 @@ impl Schedule { /// Iterates the change ticks of all systems in the schedule and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub fn check_change_ticks(&mut self, change_tick: Tick) { for system in &mut self.executable.systems { if !is_apply_deferred(system) { system.check_change_tick(change_tick); @@ -1705,10 +1705,7 @@ impl ScheduleGraph { match self.settings.hierarchy_detection { LogLevel::Ignore => unreachable!(), LogLevel::Warn => { - error!( - "Schedule {schedule_label:?} has redundant edges:\n {}", - message - ); + error!("Schedule {schedule_label:?} has redundant edges:\n {message}"); Ok(()) } LogLevel::Error => Err(ScheduleBuildError::HierarchyRedundancy(message)), @@ -1910,7 +1907,7 @@ impl ScheduleGraph { match self.settings.ambiguity_detection { LogLevel::Ignore => Ok(()), LogLevel::Warn => { - warn!("Schedule {schedule_label:?} has ambiguities.\n{}", message); + warn!("Schedule {schedule_label:?} has ambiguities.\n{message}"); Ok(()) } LogLevel::Error => Err(ScheduleBuildError::Ambiguity(message)), diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index 4974be5d43..2243e5019f 100644 --- a/crates/bevy_ecs/src/schedule/set.rs +++ b/crates/bevy_ecs/src/schedule/set.rs @@ -60,7 +60,93 @@ define_label!( ); define_label!( - /// Types that identify logical groups of systems. + /// System sets are tag-like labels that can be used to group systems together. + /// + /// This allows you to share configuration (like run conditions) across multiple systems, + /// and order systems or system sets relative to conceptual groups of systems. + /// To control the behavior of a system set as a whole, use [`Schedule::configure_sets`](crate::prelude::Schedule::configure_sets), + /// or the method of the same name on `App`. + /// + /// Systems can belong to any number of system sets, reflecting multiple roles or facets that they might have. + /// For example, you may want to annotate a system as "consumes input" and "applies forces", + /// and ensure that your systems are ordered correctly for both of those sets. + /// + /// System sets can belong to any number of other system sets, + /// allowing you to create nested hierarchies of system sets to group systems together. + /// Configuration applied to system sets will flow down to their members (including other system sets), + /// allowing you to set and modify the configuration in a single place. + /// + /// Systems sets are also useful for exposing a consistent public API for dependencies + /// to hook into across versions of your crate, + /// allowing them to add systems to a specific set, or order relative to that set, + /// without leaking implementation details of the exact systems involved. + /// + /// ## Defining new system sets + /// + /// To create a new system set, use the `#[derive(SystemSet)]` macro. + /// Unit structs are a good choice for one-off sets. + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// struct PhysicsSystems; + /// ``` + /// + /// When you want to define several related system sets, + /// consider creating an enum system set. + /// Each variant will be treated as a separate system set. + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// enum CombatSystems { + /// TargetSelection, + /// DamageCalculation, + /// Cleanup, + /// } + /// ``` + /// + /// By convention, the listed order of the system set in the enum + /// corresponds to the order in which the systems are run. + /// Ordering must be explicitly added to ensure that this is the case, + /// but following this convention will help avoid confusion. + /// + /// ### Adding systems to system sets + /// + /// To add systems to a system set, call [`in_set`](crate::prelude::IntoScheduleConfigs::in_set) on the system function + /// while adding it to your app or schedule. + /// + /// Like usual, these methods can be chained with other configuration methods like [`before`](crate::prelude::IntoScheduleConfigs::before), + /// or repeated to add systems to multiple sets. + /// + /// ```rust + /// use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// enum CombatSystems { + /// TargetSelection, + /// DamageCalculation, + /// Cleanup, + /// } + /// + /// fn target_selection() {} + /// + /// fn enemy_damage_calculation() {} + /// + /// fn player_damage_calculation() {} + /// + /// let mut schedule = Schedule::default(); + /// // Configuring the sets to run in order. + /// schedule.configure_sets((CombatSystems::TargetSelection, CombatSystems::DamageCalculation, CombatSystems::Cleanup).chain()); + /// + /// // Adding a single system to a set. + /// schedule.add_systems(target_selection.in_set(CombatSystems::TargetSelection)); + /// + /// // Adding multiple systems to a set. + /// schedule.add_systems((player_damage_calculation, enemy_damage_calculation).in_set(CombatSystems::DamageCalculation)); + /// ``` #[diagnostic::on_unimplemented( note = "consider annotating `{Self}` with `#[derive(SystemSet)]`" )] diff --git a/crates/bevy_ecs/src/schedule/stepping.rs b/crates/bevy_ecs/src/schedule/stepping.rs index 222dfdfcaf..b6de7c8215 100644 --- a/crates/bevy_ecs/src/schedule/stepping.rs +++ b/crates/bevy_ecs/src/schedule/stepping.rs @@ -475,9 +475,8 @@ impl Stepping { Some(state) => state.clear_behaviors(), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } }, @@ -486,9 +485,8 @@ impl Stepping { Some(state) => state.set_behavior(system, behavior), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } } @@ -498,9 +496,8 @@ impl Stepping { Some(state) => state.clear_behavior(system), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } } diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index 062fbb3d5b..6caa002deb 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -127,10 +127,6 @@ where self.name.clone() } - fn component_access(&self) -> &crate::query::Access { - self.system.component_access() - } - fn component_access_set( &self, ) -> &crate::query::FilteredAccessSet { diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 18de141263..95fea44985 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -4,7 +4,7 @@ use core::marker::PhantomData; use crate::{ component::{ComponentId, Tick}, prelude::World, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn, SystemParamValidationError}, world::unsafe_world_cell::UnsafeWorldCell, @@ -144,10 +144,6 @@ where self.name.clone() } - fn component_access(&self) -> &Access { - self.component_access_set.combined_access() - } - fn component_access_set(&self) -> &FilteredAccessSet { &self.component_access_set } @@ -363,10 +359,6 @@ where self.name.clone() } - fn component_access(&self) -> &Access { - self.component_access_set.combined_access() - } - fn component_access_set(&self) -> &FilteredAccessSet { &self.component_access_set } diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index f7e362a670..1cbdb5b07d 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -1,6 +1,6 @@ use crate::{ component::{ComponentId, Tick}, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, system::{ check_system_change_tick, ExclusiveSystemParam, ExclusiveSystemParamItem, IntoSystem, @@ -87,11 +87,6 @@ where self.system_meta.name.clone() } - #[inline] - fn component_access(&self) -> &Access { - self.system_meta.component_access_set.combined_access() - } - #[inline] fn component_access_set(&self) -> &FilteredAccessSet { &self.system_meta.component_access_set diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index d257a9f079..7ad7f27e2b 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -1,7 +1,7 @@ use crate::{ component::{ComponentId, Tick}, prelude::FromWorld, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, system::{ check_system_change_tick, ReadOnlySystemParam, System, SystemIn, SystemInput, SystemParam, @@ -620,11 +620,6 @@ where self.system_meta.name.clone() } - #[inline] - fn component_access(&self) -> &Access { - self.system_meta.component_access_set.combined_access() - } - #[inline] fn component_access_set(&self) -> &FilteredAccessSet { &self.system_meta.component_access_set diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 54e5a781ab..f79cb4f5d8 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -97,7 +97,7 @@ //! - [`EventWriter`](crate::event::EventWriter) //! - [`NonSend`] and `Option` //! - [`NonSendMut`] and `Option` -//! - [`RemovedComponents`](crate::removal_detection::RemovedComponents) +//! - [`RemovedComponents`](crate::lifecycle::RemovedComponents) //! - [`SystemName`] //! - [`SystemChangeTick`] //! - [`Archetypes`](crate::archetype::Archetypes) (Provides Archetype metadata) @@ -408,10 +408,10 @@ mod tests { component::{Component, Components}, entity::{Entities, Entity}, error::Result, + lifecycle::RemovedComponents, name::Name, - prelude::{AnyOf, EntityRef, Trigger}, + prelude::{AnyOf, EntityRef, OnAdd, Trigger}, query::{Added, Changed, Or, SpawnDetails, Spawned, With, Without}, - removal_detection::RemovedComponents, resource::Resource, schedule::{ common_conditions::resource_exists, ApplyDeferred, IntoScheduleConfigs, Schedule, @@ -421,7 +421,7 @@ mod tests { Commands, In, InMut, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, Res, ResMut, Single, StaticSystemParam, System, SystemState, }, - world::{DeferredWorld, EntityMut, FromWorld, OnAdd, World}, + world::{DeferredWorld, EntityMut, FromWorld, World}, }; use super::ScheduleSystem; @@ -1166,7 +1166,9 @@ mod tests { x.initialize(&mut world); y.initialize(&mut world); - let conflicts = x.component_access().get_conflicts(y.component_access()); + let conflicts = x + .component_access_set() + .get_conflicts(y.component_access_set()); let b_id = world .components() .get_resource_id(TypeId::of::()) diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index b123301a08..4891a39d45 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -6,7 +6,7 @@ use crate::{ error::Result, never::Never, prelude::{Bundle, Trigger}, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::{Fallible, Infallible}, system::{input::SystemIn, System}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, @@ -116,11 +116,6 @@ where self.observer.name() } - #[inline] - fn component_access(&self) -> &Access { - self.observer.component_access() - } - #[inline] fn component_access_set(&self) -> &FilteredAccessSet { self.observer.component_access_set() diff --git a/crates/bevy_ecs/src/system/schedule_system.rs b/crates/bevy_ecs/src/system/schedule_system.rs index 17bd3f46de..5e05a5ada9 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -3,7 +3,7 @@ use alloc::{borrow::Cow, vec::Vec}; use crate::{ component::{ComponentId, Tick}, error::Result, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, system::{input::SystemIn, BoxedSystem, System, SystemInput}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FromWorld, World}, }; @@ -33,11 +33,6 @@ impl> System for InfallibleSystemWrapper { self.0.type_id() } - #[inline] - fn component_access(&self) -> &Access { - self.0.component_access() - } - #[inline] fn component_access_set(&self) -> &FilteredAccessSet { self.0.component_access_set() @@ -154,10 +149,6 @@ where self.system.name() } - fn component_access(&self) -> &Access { - self.system.component_access() - } - fn component_access_set(&self) -> &FilteredAccessSet { self.system.component_access_set() } @@ -256,10 +247,6 @@ where self.system.name() } - fn component_access(&self) -> &Access { - self.system.component_access() - } - fn component_access_set(&self) -> &FilteredAccessSet { self.system.component_access_set() } diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 211152f3d8..fc96e8a843 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -9,7 +9,7 @@ use thiserror::Error; use crate::{ component::{ComponentId, Tick}, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, @@ -57,9 +57,6 @@ pub trait System: Send + Sync + 'static { TypeId::of::() } - /// Returns the system's component [`Access`]. - fn component_access(&self) -> &Access; - /// Returns the system's component [`FilteredAccessSet`]. fn component_access_set(&self) -> &FilteredAccessSet; diff --git a/crates/bevy_ecs/src/world/component_constants.rs b/crates/bevy_ecs/src/world/component_constants.rs deleted file mode 100644 index ea2899c5f9..0000000000 --- a/crates/bevy_ecs/src/world/component_constants.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Internal components used by bevy with a fixed component id. -//! Constants are used to skip [`TypeId`] lookups in hot paths. -use super::*; -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::Reflect; - -/// [`ComponentId`] for [`OnAdd`] -pub const ON_ADD: ComponentId = ComponentId::new(0); -/// [`ComponentId`] for [`OnInsert`] -pub const ON_INSERT: ComponentId = ComponentId::new(1); -/// [`ComponentId`] for [`OnReplace`] -pub const ON_REPLACE: ComponentId = ComponentId::new(2); -/// [`ComponentId`] for [`OnRemove`] -pub const ON_REMOVE: ComponentId = ComponentId::new(3); -/// [`ComponentId`] for [`OnDespawn`] -pub const ON_DESPAWN: ComponentId = ComponentId::new(4); - -/// Trigger emitted when a component is inserted onto an entity that does not already have that -/// component. Runs before `OnInsert`. -/// See [`crate::component::ComponentHooks::on_add`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnAdd; - -/// Trigger emitted when a component is inserted, regardless of whether or not the entity already -/// had that component. Runs after `OnAdd`, if it ran. -/// See [`crate::component::ComponentHooks::on_insert`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnInsert; - -/// Trigger emitted when a component is inserted onto an entity that already has that component. -/// Runs before the value is replaced, so you can still access the original component data. -/// See [`crate::component::ComponentHooks::on_replace`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnReplace; - -/// Trigger emitted when a component is removed from an entity, and runs before the component is -/// removed, so you can still access the component data. -/// See [`crate::component::ComponentHooks::on_remove`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnRemove; - -/// Trigger emitted for each component on an entity when it is despawned. -/// See [`crate::component::ComponentHooks::on_despawn`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnDespawn; diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 5a20046b2f..da51b8e09a 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -3,9 +3,10 @@ use core::ops::Deref; use crate::{ archetype::Archetype, change_detection::{MaybeLocation, MutUntyped}, - component::{ComponentId, HookContext, Mutable}, + component::{ComponentId, Mutable}, entity::Entity, event::{Event, EventId, Events, SendBatchIds}, + lifecycle::{HookContext, ON_INSERT, ON_REPLACE}, observer::{Observers, TriggerTargets}, prelude::{Component, QueryState}, query::{QueryData, QueryFilter}, @@ -16,7 +17,7 @@ use crate::{ world::{error::EntityMutableFetchError, EntityFetcher, WorldEntityFetch}, }; -use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World, ON_INSERT, ON_REPLACE}; +use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World}; /// A [`World`] reference that disallows structural ECS changes. /// This includes initializing resources, registering components or spawning entities. diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index dd653831d9..af95bbf48d 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -14,15 +14,13 @@ use crate::{ EntityIdLocation, EntityLocation, }, event::Event, + lifecycle::{ON_DESPAWN, ON_REMOVE, ON_REPLACE}, observer::Observer, query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData}, relationship::RelationshipHookMode, resource::Resource, system::IntoObserverSystem, - world::{ - error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World, - ON_DESPAWN, ON_REMOVE, ON_REPLACE, - }, + world::{error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World}, }; use alloc::vec::Vec; use bevy_platform::collections::{HashMap, HashSet}; @@ -2609,14 +2607,14 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. - pub fn entry<'a, T: Component>(&'a mut self) -> Entry<'w, 'a, T> { + pub fn entry<'a, T: Component>(&'a mut self) -> ComponentEntry<'w, 'a, T> { if self.contains::() { - Entry::Occupied(OccupiedEntry { + ComponentEntry::Occupied(OccupiedComponentEntry { entity_world: self, _marker: PhantomData, }) } else { - Entry::Vacant(VacantEntry { + ComponentEntry::Vacant(VacantComponentEntry { entity_world: self, _marker: PhantomData, }) @@ -2859,14 +2857,14 @@ impl<'w> EntityWorldMut<'w> { /// This `enum` can only be constructed from the [`entry`] method on [`EntityWorldMut`]. /// /// [`entry`]: EntityWorldMut::entry -pub enum Entry<'w, 'a, T: Component> { +pub enum ComponentEntry<'w, 'a, T: Component> { /// An occupied entry. - Occupied(OccupiedEntry<'w, 'a, T>), + Occupied(OccupiedComponentEntry<'w, 'a, T>), /// A vacant entry. - Vacant(VacantEntry<'w, 'a, T>), + Vacant(VacantComponentEntry<'w, 'a, T>), } -impl<'w, 'a, T: Component> Entry<'w, 'a, T> { +impl<'w, 'a, T: Component> ComponentEntry<'w, 'a, T> { /// Provides in-place mutable access to an occupied entry. /// /// # Examples @@ -2885,17 +2883,17 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { #[inline] pub fn and_modify)>(self, f: F) -> Self { match self { - Entry::Occupied(mut entry) => { + ComponentEntry::Occupied(mut entry) => { f(entry.get_mut()); - Entry::Occupied(entry) + ComponentEntry::Occupied(entry) } - Entry::Vacant(entry) => Entry::Vacant(entry), + ComponentEntry::Vacant(entry) => ComponentEntry::Vacant(entry), } } } -impl<'w, 'a, T: Component> Entry<'w, 'a, T> { - /// Replaces the component of the entry, and returns an [`OccupiedEntry`]. +impl<'w, 'a, T: Component> ComponentEntry<'w, 'a, T> { + /// Replaces the component of the entry, and returns an [`OccupiedComponentEntry`]. /// /// # Examples /// @@ -2914,13 +2912,13 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(entry.get(), &Comp(2)); /// ``` #[inline] - pub fn insert_entry(self, component: T) -> OccupiedEntry<'w, 'a, T> { + pub fn insert_entry(self, component: T) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(mut entry) => { + ComponentEntry::Occupied(mut entry) => { entry.insert(component); entry } - Entry::Vacant(entry) => entry.insert(component), + ComponentEntry::Vacant(entry) => entry.insert(component), } } @@ -2946,10 +2944,10 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 8); /// ``` #[inline] - pub fn or_insert(self, default: T) -> OccupiedEntry<'w, 'a, T> { + pub fn or_insert(self, default: T) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(default), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(default), } } @@ -2970,15 +2968,15 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 4); /// ``` #[inline] - pub fn or_insert_with T>(self, default: F) -> OccupiedEntry<'w, 'a, T> { + pub fn or_insert_with T>(self, default: F) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(default()), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(default()), } } } -impl<'w, 'a, T: Component + Default> Entry<'w, 'a, T> { +impl<'w, 'a, T: Component + Default> ComponentEntry<'w, 'a, T> { /// Ensures the entry has this component by inserting the default value if empty, and /// returns a mutable reference to this component in the entry. /// @@ -2996,42 +2994,42 @@ impl<'w, 'a, T: Component + Default> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 0); /// ``` #[inline] - pub fn or_default(self) -> OccupiedEntry<'w, 'a, T> { + pub fn or_default(self) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(Default::default()), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(Default::default()), } } } -/// A view into an occupied entry in a [`EntityWorldMut`]. It is part of the [`Entry`] enum. +/// A view into an occupied entry in a [`EntityWorldMut`]. It is part of the [`OccupiedComponentEntry`] enum. /// /// The contained entity must have the component type parameter if we have this struct. -pub struct OccupiedEntry<'w, 'a, T: Component> { +pub struct OccupiedComponentEntry<'w, 'a, T: Component> { entity_world: &'a mut EntityWorldMut<'w>, _marker: PhantomData, } -impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { +impl<'w, 'a, T: Component> OccupiedComponentEntry<'w, 'a, T> { /// Gets a reference to the component in the entry. /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// assert_eq!(o.get().0, 5); /// } /// ``` #[inline] pub fn get(&self) -> &T { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get::().unwrap() } @@ -3040,14 +3038,14 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(mut o) = entity.entry::() { + /// if let ComponentEntry::Occupied(mut o) = entity.entry::() { /// o.insert(Comp(10)); /// } /// @@ -3063,14 +3061,14 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// assert_eq!(o.take(), Comp(5)); /// } /// @@ -3078,30 +3076,30 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn take(self) -> T { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.take().unwrap() } } -impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { +impl<'w, 'a, T: Component> OccupiedComponentEntry<'w, 'a, T> { /// Gets a mutable reference to the component in the entry. /// - /// If you need a reference to the `OccupiedEntry` which may outlive the destruction of - /// the `Entry` value, see [`into_mut`]. + /// If you need a reference to the [`OccupiedComponentEntry`] which may outlive the destruction of + /// the [`OccupiedComponentEntry`] value, see [`into_mut`]. /// /// [`into_mut`]: Self::into_mut /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(mut o) = entity.entry::() { + /// if let ComponentEntry::Occupied(mut o) = entity.entry::() { /// o.get_mut().0 += 10; /// assert_eq!(o.get().0, 15); /// @@ -3113,28 +3111,28 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn get_mut(&mut self) -> Mut<'_, T> { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get_mut::().unwrap() } - /// Converts the `OccupiedEntry` into a mutable reference to the value in the entry with + /// Converts the [`OccupiedComponentEntry`] into a mutable reference to the value in the entry with /// a lifetime bound to the `EntityWorldMut`. /// - /// If you need multiple references to the `OccupiedEntry`, see [`get_mut`]. + /// If you need multiple references to the [`OccupiedComponentEntry`], see [`get_mut`]. /// /// [`get_mut`]: Self::get_mut /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// o.into_mut().0 += 10; /// } /// @@ -3142,40 +3140,40 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn into_mut(self) -> Mut<'a, T> { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get_mut().unwrap() } } -/// A view into a vacant entry in a [`EntityWorldMut`]. It is part of the [`Entry`] enum. -pub struct VacantEntry<'w, 'a, T: Component> { +/// A view into a vacant entry in a [`EntityWorldMut`]. It is part of the [`ComponentEntry`] enum. +pub struct VacantComponentEntry<'w, 'a, T: Component> { entity_world: &'a mut EntityWorldMut<'w>, _marker: PhantomData, } -impl<'w, 'a, T: Component> VacantEntry<'w, 'a, T> { - /// Inserts the component into the `VacantEntry` and returns an `OccupiedEntry`. +impl<'w, 'a, T: Component> VacantComponentEntry<'w, 'a, T> { + /// Inserts the component into the [`VacantComponentEntry`] and returns an [`OccupiedComponentEntry`]. /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn_empty(); /// - /// if let Entry::Vacant(v) = entity.entry::() { + /// if let ComponentEntry::Vacant(v) = entity.entry::() { /// v.insert(Comp(10)); /// } /// /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 10); /// ``` #[inline] - pub fn insert(self, component: T) -> OccupiedEntry<'w, 'a, T> { + pub fn insert(self, component: T) -> OccupiedComponentEntry<'w, 'a, T> { self.entity_world.insert(component); - OccupiedEntry { + OccupiedComponentEntry { entity_world: self.entity_world, _marker: PhantomData, } @@ -3186,7 +3184,7 @@ impl<'w, 'a, T: Component> VacantEntry<'w, 'a, T> { /// /// To define the access when used as a [`QueryData`](crate::query::QueryData), /// use a [`QueryBuilder`](crate::query::QueryBuilder) or [`QueryParamBuilder`](crate::system::QueryParamBuilder). -/// The `FilteredEntityRef` must be the entire `QueryData`, and not nested inside a tuple with other data. +/// The [`FilteredEntityRef`] must be the entire [`QueryData`](crate::query::QueryData), and not nested inside a tuple with other data. /// /// ``` /// # use bevy_ecs::{prelude::*, world::FilteredEntityRef}; @@ -4756,7 +4754,8 @@ mod tests { use core::panic::AssertUnwindSafe; use std::sync::OnceLock; - use crate::component::{HookContext, Tick}; + use crate::component::Tick; + use crate::lifecycle::HookContext; use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::ComponentId, diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 4172f0b31d..34d4cd9c4a 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1,7 +1,6 @@ //! Defines the [`World`] and APIs for accessing it directly. pub(crate) mod command_queue; -mod component_constants; mod deferred_world; mod entity_fetch; mod entity_ref; @@ -14,18 +13,22 @@ pub mod unsafe_world_cell; #[cfg(feature = "bevy_reflect")] pub mod reflect; -use crate::error::{DefaultErrorHandler, ErrorHandler}; pub use crate::{ change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}, world::command_queue::CommandQueue, }; +use crate::{ + error::{DefaultErrorHandler, ErrorHandler}, + lifecycle::{ComponentHooks, ON_ADD, ON_DESPAWN, ON_INSERT, ON_REMOVE, ON_REPLACE}, + prelude::{OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace}, +}; pub use bevy_ecs_macros::FromWorld; -pub use component_constants::*; pub use deferred_world::DeferredWorld; pub use entity_fetch::{EntityFetcher, WorldEntityFetch}; pub use entity_ref::{ - DynamicComponentFetch, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, EntityWorldMut, - Entry, FilteredEntityMut, FilteredEntityRef, OccupiedEntry, TryFromFilteredError, VacantEntry, + ComponentEntry, DynamicComponentFetch, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, + EntityWorldMut, FilteredEntityMut, FilteredEntityRef, OccupiedComponentEntry, + TryFromFilteredError, VacantComponentEntry, }; pub use filtered_resource::*; pub use identifier::WorldId; @@ -39,17 +42,17 @@ use crate::{ }, change_detection::{MaybeLocation, MutUntyped, TicksMut}, component::{ - Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentIds, ComponentInfo, + CheckChangeTicks, Component, ComponentDescriptor, ComponentId, ComponentIds, ComponentInfo, ComponentTicks, Components, ComponentsQueuedRegistrator, ComponentsRegistrator, Mutable, RequiredComponents, RequiredComponentsError, Tick, }, entity::{Entities, Entity, EntityDoesNotExistError}, entity_disabling::DefaultQueryFilters, event::{Event, EventId, Events, SendBatchIds}, + lifecycle::RemovedComponentEvents, observer::Observers, query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState}, relationship::RelationshipHookMode, - removal_detection::RemovedComponentEvents, resource::Resource, schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, @@ -1444,7 +1447,7 @@ impl World { /// assert!(!transform.is_changed()); /// ``` /// - /// [`RemovedComponents`]: crate::removal_detection::RemovedComponents + /// [`RemovedComponents`]: crate::lifecycle::RemovedComponents pub fn clear_trackers(&mut self) { self.removed_components.update(); self.last_change_tick = self.increment_change_tick(); @@ -2964,6 +2967,8 @@ impl World { schedules.check_change_ticks(change_tick); } + self.trigger(CheckChangeTicks(change_tick)); + self.last_check_tick = change_tick; } diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 1959f3e5f3..ca898f9531 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -8,10 +8,10 @@ use crate::{ component::{ComponentId, ComponentTicks, Components, Mutable, StorageType, Tick, TickCells}, entity::{ContainsEntity, Entities, Entity, EntityDoesNotExistError, EntityLocation}, error::{DefaultErrorHandler, ErrorHandler}, + lifecycle::RemovedComponentEvents, observer::Observers, prelude::Component, query::{DebugCheckedUnwrap, ReadOnlyQueryData}, - removal_detection::RemovedComponentEvents, resource::Resource, storage::{ComponentSparseSet, Storages, Table}, world::RawCommandQueue, @@ -36,7 +36,7 @@ use thiserror::Error; /// /// This alone is not enough to implement bevy systems where multiple systems can access *disjoint* parts of the world concurrently. For this, bevy stores all values of /// resources and components (and [`ComponentTicks`]) in [`UnsafeCell`]s, and carefully validates disjoint access patterns using -/// APIs like [`System::component_access`](crate::system::System::component_access). +/// APIs like [`System::component_access_set`](crate::system::System::component_access_set). /// /// A system then can be executed using [`System::run_unsafe`](crate::system::System::run_unsafe) with a `&World` and use methods with interior mutability to access resource values. /// diff --git a/crates/bevy_input_focus/src/autofocus.rs b/crates/bevy_input_focus/src/autofocus.rs index 72024418d2..f6a862db88 100644 --- a/crates/bevy_input_focus/src/autofocus.rs +++ b/crates/bevy_input_focus/src/autofocus.rs @@ -1,6 +1,6 @@ //! Contains the [`AutoFocus`] component and related machinery. -use bevy_ecs::{component::HookContext, prelude::*, world::DeferredWorld}; +use bevy_ecs::{lifecycle::HookContext, prelude::*, world::DeferredWorld}; use crate::InputFocus; diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index e8879d7a9c..436e838fb1 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -369,7 +369,7 @@ mod tests { use alloc::string::String; use bevy_ecs::{ - component::HookContext, observer::Trigger, system::RunSystemOnce, world::DeferredWorld, + lifecycle::HookContext, observer::Trigger, system::RunSystemOnce, world::DeferredWorld, }; use bevy_input::{ keyboard::{Key, KeyCode}, diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 9f78fc009d..e22702e348 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -395,6 +395,7 @@ bevy_color = { path = "../bevy_color", optional = true, version = "0.16.0-dev", "bevy_reflect", ] } bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.16.0-dev" } +bevy_core_widgets = { path = "../bevy_core_widgets", optional = true, version = "0.16.0-dev" } bevy_anti_aliasing = { path = "../bevy_anti_aliasing", optional = true, version = "0.16.0-dev" } bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.16.0-dev" } bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.16.0-dev" } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 274364882e..b5446d6b85 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -29,6 +29,8 @@ pub use bevy_audio as audio; pub use bevy_color as color; #[cfg(feature = "bevy_core_pipeline")] pub use bevy_core_pipeline as core_pipeline; +#[cfg(feature = "bevy_core_widgets")] +pub use bevy_core_widgets as core_widgets; #[cfg(feature = "bevy_dev_tools")] pub use bevy_dev_tools as dev_tools; pub use bevy_diagnostic as diagnostic; diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index e6d8899d63..fce602f7ad 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -45,7 +45,7 @@ bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = fa ] } [target.'cfg(target_os = "ios")'.dependencies] -tracing-oslog = "0.2" +tracing-oslog = "0.3" [lints] workspace = true diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index cb7aecc2cb..567bbce674 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -37,9 +37,9 @@ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, + lifecycle::RemovedComponents, query::{Changed, Or}, reflect::ReflectComponent, - removal_detection::RemovedComponents, resource::Resource, schedule::IntoScheduleConfigs, system::{Query, Res, ResMut}, diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 72c0f06c46..f9083433be 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -31,7 +31,7 @@ //! //! The events this module defines fall into a few broad categories: //! + Hovering and movement: [`Over`], [`Move`], and [`Out`]. -//! + Clicking and pressing: [`Pressed`], [`Released`], and [`Click`]. +//! + Clicking and pressing: [`Press`], [`Release`], and [`Click`]. //! + Dragging and dropping: [`DragStart`], [`Drag`], [`DragEnd`], [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`]. //! //! When received by an observer, these events will always be wrapped by the [`Pointer`] type, which contains @@ -171,7 +171,7 @@ pub struct Out { /// Fires when a pointer button is pressed over the `target` entity. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] -pub struct Pressed { +pub struct Press { /// Pointer button pressed to trigger this event. pub button: PointerButton, /// Information about the picking intersection. @@ -181,7 +181,7 @@ pub struct Pressed { /// Fires when a pointer button is released over the `target` entity. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] -pub struct Released { +pub struct Release { /// Pointer button lifted to trigger this event. pub button: PointerButton, /// Information about the picking intersection. @@ -400,7 +400,7 @@ impl PointerState { pub struct PickingEventWriters<'w> { cancel_events: EventWriter<'w, Pointer>, click_events: EventWriter<'w, Pointer>, - pressed_events: EventWriter<'w, Pointer>, + pressed_events: EventWriter<'w, Pointer>, drag_drop_events: EventWriter<'w, Pointer>, drag_end_events: EventWriter<'w, Pointer>, drag_enter_events: EventWriter<'w, Pointer>, @@ -412,7 +412,7 @@ pub struct PickingEventWriters<'w> { move_events: EventWriter<'w, Pointer>, out_events: EventWriter<'w, Pointer>, over_events: EventWriter<'w, Pointer>, - released_events: EventWriter<'w, Pointer>, + released_events: EventWriter<'w, Pointer>, } /// Dispatches interaction events to the target entities. @@ -422,7 +422,7 @@ pub struct PickingEventWriters<'w> { /// + [`DragEnter`] → [`Over`]. /// + Any number of any of the following: /// + For each movement: [`DragStart`] → [`Drag`] → [`DragOver`] → [`Move`]. -/// + For each button press: [`Pressed`] or [`Click`] → [`Released`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`]. +/// + For each button press: [`Press`] or [`Click`] → [`Release`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`]. /// + For each pointer cancellation: [`Cancel`]. /// /// Additionally, across multiple frames, the following are also strictly @@ -430,7 +430,7 @@ pub struct PickingEventWriters<'w> { /// + When a pointer moves over the target: /// [`Over`], [`Move`], [`Out`]. /// + When a pointer presses buttons on the target: -/// [`Pressed`], [`Click`], [`Released`]. +/// [`Press`], [`Click`], [`Release`]. /// + When a pointer drags the target: /// [`DragStart`], [`Drag`], [`DragEnd`]. /// + When a pointer drags something over the target: @@ -452,7 +452,7 @@ pub struct PickingEventWriters<'w> { /// In the context of UI, this is especially problematic. Additional hierarchy-aware /// events will be added in a future release. /// -/// Both [`Click`] and [`Released`] target the entity hovered in the *previous frame*, +/// Both [`Click`] and [`Release`] target the entity hovered in the *previous frame*, /// rather than the current frame. This is because touch pointers hover nothing /// on the frame they are released. The end effect is that these two events can /// be received sequentially after an [`Out`] event (but always on the same frame @@ -609,7 +609,7 @@ pub fn pointer_events( pointer_id, location.clone(), hovered_entity, - Pressed { + Press { button, hit: hit.clone(), }, @@ -646,12 +646,12 @@ pub fn pointer_events( commands.trigger_targets(click_event.clone(), hovered_entity); event_writers.click_events.write(click_event); } - // Always send the Released event + // Always send the Release event let released_event = Pointer::new( pointer_id, location.clone(), hovered_entity, - Released { + Release { button, hit: hit.clone(), }, diff --git a/crates/bevy_picking/src/hover.rs b/crates/bevy_picking/src/hover.rs index 2bf23c50ba..dbb6ee942e 100644 --- a/crates/bevy_picking/src/hover.rs +++ b/crates/bevy_picking/src/hover.rs @@ -14,7 +14,7 @@ use crate::{ }; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::prelude::*; +use bevy_ecs::{entity::EntityHashSet, prelude::*}; use bevy_math::FloatOrd; use bevy_platform::collections::HashMap; use bevy_reflect::prelude::*; @@ -279,3 +279,285 @@ fn merge_interaction_states( new_interaction_state.insert(*hovered_entity, new_interaction); } } + +/// A component that allows users to use regular Bevy change detection to determine when the pointer +/// enters or leaves an entity. Users should insert this component on an entity to indicate interest +/// in knowing about hover state changes. +/// +/// The component's boolean value will be `true` whenever the pointer is currently directly hovering +/// over the entity, or any of the entity's descendants (as defined by the [`ChildOf`] +/// relationship). This is consistent with the behavior of the CSS `:hover` pseudo-class, which +/// applies to the element and all of its descendants. +/// +/// The contained boolean value is guaranteed to only be mutated when the pointer enters or leaves +/// the entity, allowing Bevy change detection to be used efficiently. This is in contrast to the +/// [`HoverMap`] resource, which is updated every frame. +/// +/// Typically, a simple hoverable entity or widget will have this component added to it. More +/// complex widgets can have this component added to each hoverable part. +/// +/// The computational cost of keeping the `Hovered` components up to date is relatively cheap, and +/// linear in the number of entities that have the [`Hovered`] component inserted. +#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[component(immutable)] +pub struct Hovered(pub bool); + +impl Hovered { + /// Get whether the entity is currently hovered. + pub fn get(&self) -> bool { + self.0 + } +} + +/// A component that allows users to use regular Bevy change detection to determine when the pointer +/// is directly hovering over an entity. Users should insert this component on an entity to indicate +/// interest in knowing about hover state changes. +/// +/// This is similar to [`Hovered`] component, except that it does not include descendants in the +/// hover state. +#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[component(immutable)] +pub struct DirectlyHovered(pub bool); + +impl DirectlyHovered { + /// Get whether the entity is currently hovered. + pub fn get(&self) -> bool { + self.0 + } +} + +/// Uses [`HoverMap`] changes to update [`Hovered`] components. +pub fn update_is_hovered( + hover_map: Option>, + mut hovers: Query<(Entity, &Hovered)>, + parent_query: Query<&ChildOf>, + mut commands: Commands, +) { + // Don't do any work if there's no hover map. + let Some(hover_map) = hover_map else { return }; + + // Don't bother collecting ancestors if there are no hovers. + if hovers.is_empty() { + return; + } + + // Algorithm: for each entity having a `Hovered` component, we want to know if the current + // entry in the hover map is "within" (that is, in the set of descenants of) that entity. Rather + // than doing an expensive breadth-first traversal of children, instead start with the hovermap + // entry and search upwards. We can make this even cheaper by building a set of ancestors for + // the hovermap entry, and then testing each `Hovered` entity against that set. + + // A set which contains the hovered for the current pointer entity and its ancestors. The + // capacity is based on the likely tree depth of the hierarchy, which is typically greater for + // UI (because of layout issues) than for 3D scenes. A depth of 32 is a reasonable upper bound + // for most use cases. + let mut hover_ancestors = EntityHashSet::with_capacity(32); + if let Some(map) = hover_map.get(&PointerId::Mouse) { + for hovered_entity in map.keys() { + hover_ancestors.insert(*hovered_entity); + hover_ancestors.extend(parent_query.iter_ancestors(*hovered_entity)); + } + } + + // For each hovered entity, it is considered "hovering" if it's in the set of hovered ancestors. + for (entity, hoverable) in hovers.iter_mut() { + let is_hovering = hover_ancestors.contains(&entity); + if hoverable.0 != is_hovering { + commands.entity(entity).insert(Hovered(is_hovering)); + } + } +} + +/// Uses [`HoverMap`] changes to update [`DirectlyHovered`] components. +pub fn update_is_directly_hovered( + hover_map: Option>, + hovers: Query<(Entity, &DirectlyHovered)>, + mut commands: Commands, +) { + // Don't do any work if there's no hover map. + let Some(hover_map) = hover_map else { return }; + + // Don't bother collecting ancestors if there are no hovers. + if hovers.is_empty() { + return; + } + + if let Some(map) = hover_map.get(&PointerId::Mouse) { + // It's hovering if it's in the HoverMap. + for (entity, hoverable) in hovers.iter() { + let is_hovering = map.contains_key(&entity); + if hoverable.0 != is_hovering { + commands.entity(entity).insert(DirectlyHovered(is_hovering)); + } + } + } else { + // No hovered entity, reset all hovers. + for (entity, hoverable) in hovers.iter() { + if hoverable.0 { + commands.entity(entity).insert(DirectlyHovered(false)); + } + } + } +} + +#[cfg(test)] +mod tests { + use bevy_render::camera::Camera; + + use super::*; + + #[test] + fn update_is_hovered_memoized() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_child = world.spawn_empty().id(); + let hovered_entity = world.spawn(Hovered(false)).add_child(hovered_child).id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_child, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_hovered).is_ok()); + + // Check to insure that the hovered entity has the Hovered component set to true + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(hover.get()); + assert!(hover.is_changed()); + + // Now do it again, but don't change the hover map. + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_hovered).is_ok()); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(hover.get()); + + // Should not be changed + // NOTE: Test doesn't work - thinks it is always changed + // assert!(!hover.is_changed()); + + // Clear the hover map and run again. + world.insert_resource(HoverMap::default()); + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_hovered).is_ok()); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } + + #[test] + fn update_is_hovered_direct_self() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_entity = world.spawn(DirectlyHovered(false)).id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_entity, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + + // Check to insure that the hovered entity has the DirectlyHovered component set to true + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(hover.get()); + assert!(hover.is_changed()); + + // Now do it again, but don't change the hover map. + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(hover.get()); + + // Should not be changed + // NOTE: Test doesn't work - thinks it is always changed + // assert!(!hover.is_changed()); + + // Clear the hover map and run again. + world.insert_resource(HoverMap::default()); + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } + + #[test] + fn update_is_hovered_direct_child() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_child = world.spawn_empty().id(); + let hovered_entity = world + .spawn(DirectlyHovered(false)) + .add_child(hovered_child) + .id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_child, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + + // Check to insure that the DirectlyHovered component is still false + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } +} diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 70a5714581..6a7576f132 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -170,6 +170,7 @@ pub mod window; use bevy_app::{prelude::*, PluginGroupBuilder}; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; +use hover::{update_is_directly_hovered, update_is_hovered}; /// The picking prelude. /// @@ -392,6 +393,7 @@ impl Plugin for PickingPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -414,7 +416,7 @@ impl Plugin for InteractionPlugin { .init_resource::() .add_event::>() .add_event::>() - .add_event::>() + .add_event::>() .add_event::>() .add_event::>() .add_event::>() @@ -425,11 +427,16 @@ impl Plugin for InteractionPlugin { .add_event::>() .add_event::>() .add_event::>() - .add_event::>() + .add_event::>() .add_event::>() .add_systems( PreUpdate, - (generate_hovermap, update_interactions, pointer_events) + ( + generate_hovermap, + update_interactions, + (update_is_hovered, update_is_directly_hovered), + pointer_events, + ) .chain() .in_set(PickingSystems::Hover), ); diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index c599894b89..1c72714802 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -8,9 +8,9 @@ use bevy_ecs::{ entity::Entity, event::EventCursor, hierarchy::ChildOf, + lifecycle::RemovedComponentEntity, query::QueryBuilder, reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}, - removal_detection::RemovedComponentEntity, system::{In, Local}, world::{EntityRef, EntityWorldMut, FilteredEntityRef, World}, }; diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index d97950240d..fd3b8cb4b2 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -22,9 +22,10 @@ use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChanges, - component::{Component, HookContext}, + component::Component, entity::{ContainsEntity, Entity}, event::EventReader, + lifecycle::HookContext, prelude::With, query::Has, reflect::ReflectComponent, @@ -715,12 +716,13 @@ impl Camera { } } -/// Control how this camera outputs once rendering is completed. +/// Control how this [`Camera`] outputs once rendering is completed. #[derive(Debug, Clone, Copy)] pub enum CameraOutputMode { /// Writes the camera output to configured render target. Write { /// The blend state that will be used by the pipeline that writes the intermediate render textures to the final render target texture. + /// If not set, the output will be written as-is, ignoring `clear_color` and the existing data in the final render target texture. blend_state: Option, /// The clear color operation to perform on the final render target texture. clear_color: ClearColorConfig, diff --git a/crates/bevy_render/src/camera/clear_color.rs b/crates/bevy_render/src/camera/clear_color.rs index 157bcf8998..6183a1d4de 100644 --- a/crates/bevy_render/src/camera/clear_color.rs +++ b/crates/bevy_render/src/camera/clear_color.rs @@ -6,7 +6,9 @@ use bevy_reflect::prelude::*; use derive_more::derive::From; use serde::{Deserialize, Serialize}; -/// For a camera, specifies the color used to clear the viewport before rendering. +/// For a camera, specifies the color used to clear the viewport +/// [before rendering](crate::camera::Camera::clear_color) +/// or when [writing to the final render target texture](crate::camera::Camera::output_mode). #[derive(Reflect, Serialize, Deserialize, Copy, Clone, Debug, Default, From)] #[reflect(Serialize, Deserialize, Default, Clone)] pub enum ClearColorConfig { @@ -21,10 +23,15 @@ pub enum ClearColorConfig { None, } -/// A [`Resource`] that stores the color that is used to clear the screen between frames. +/// A [`Resource`] that stores the default color that cameras use to clear the screen between frames. /// /// This color appears as the "background" color for simple apps, /// when there are portions of the screen with nothing rendered. +/// +/// Individual cameras may use [`Camera.clear_color`] to specify a different +/// clear color or opt out of clearing their viewport. +/// +/// [`Camera.clear_color`]: crate::camera::Camera::clear_color #[derive(Resource, Clone, Debug, Deref, DerefMut, ExtractResource, Reflect)] #[reflect(Resource, Default, Debug, Clone)] pub struct ClearColor(pub Color); diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index eb2d4de626..c171cf3957 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -78,6 +78,9 @@ pub struct MeshAllocator { /// WebGL 2. On this platform, we must give each vertex array its own /// buffer, because we can't adjust the first vertex when we perform a draw. general_vertex_slabs_supported: bool, + + /// Additional buffer usages to add to any vertex or index buffers created. + pub extra_buffer_usages: BufferUsages, } /// Tunable parameters that customize the behavior of the allocator. @@ -348,6 +351,7 @@ impl FromWorld for MeshAllocator { mesh_id_to_index_slab: HashMap::default(), next_slab_id: default(), general_vertex_slabs_supported, + extra_buffer_usages: BufferUsages::empty(), } } } @@ -598,7 +602,7 @@ impl MeshAllocator { buffer_usages_to_str(buffer_usages) )), size: len as u64, - usage: buffer_usages | BufferUsages::COPY_DST, + usage: buffer_usages | BufferUsages::COPY_DST | self.extra_buffer_usages, mapped_at_creation: true, }); { @@ -835,7 +839,7 @@ impl MeshAllocator { buffer_usages_to_str(buffer_usages) )), size: slab.current_slot_capacity as u64 * slab.element_layout.slot_size(), - usage: buffer_usages, + usage: buffer_usages | self.extra_buffer_usages, mapped_at_creation: false, }); diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index 90613451dd..dc7aadafbc 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -1,6 +1,7 @@ use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::entity::EntityHash; +use bevy_ecs::lifecycle::{OnAdd, OnRemove}; use bevy_ecs::{ component::Component, entity::{ContainsEntity, Entity, EntityEquivalent}, @@ -9,7 +10,7 @@ use bevy_ecs::{ reflect::ReflectComponent, resource::Resource, system::{Local, Query, ResMut, SystemState}, - world::{Mut, OnAdd, OnRemove, World}, + world::{Mut, World}, }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -219,10 +220,10 @@ pub(crate) fn entity_sync_system(main_world: &mut World, render_world: &mut Worl EntityRecord::Added(e) => { if let Ok(mut main_entity) = world.get_entity_mut(e) { match main_entity.entry::() { - bevy_ecs::world::Entry::Occupied(_) => { + bevy_ecs::world::ComponentEntry::Occupied(_) => { panic!("Attempting to synchronize an entity that has already been synchronized!"); } - bevy_ecs::world::Entry::Vacant(entry) => { + bevy_ecs::world::ComponentEntry::Vacant(entry) => { let id = render_world.spawn(MainEntity(e)).id(); entry.insert(RenderEntity(id)); @@ -490,10 +491,11 @@ mod tests { use bevy_ecs::{ component::Component, entity::Entity, + lifecycle::{OnAdd, OnRemove}, observer::Trigger, query::With, system::{Query, ResMut}, - world::{OnAdd, OnRemove, World}, + world::World, }; use super::{ diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 43533b5354..13b8ac74d4 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -3,8 +3,8 @@ mod render_layers; use core::any::TypeId; -use bevy_ecs::component::HookContext; use bevy_ecs::entity::EntityHashSet; +use bevy_ecs::lifecycle::HookContext; use bevy_ecs::world::DeferredWorld; use derive_more::derive::{Deref, DerefMut}; pub use range::*; diff --git a/crates/bevy_render/src/view/visibility/range.rs b/crates/bevy_render/src/view/visibility/range.rs index 2559d3b8d2..80f89ce936 100644 --- a/crates/bevy_render/src/view/visibility/range.rs +++ b/crates/bevy_render/src/view/visibility/range.rs @@ -10,9 +10,9 @@ use bevy_app::{App, Plugin, PostUpdate}; use bevy_ecs::{ component::Component, entity::{Entity, EntityHashMap}, + lifecycle::RemovedComponents, query::{Changed, With}, reflect::ReflectComponent, - removal_detection::RemovedComponents, resource::Resource, schedule::IntoScheduleConfigs as _, system::{Local, Query, Res, ResMut}, diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index 70b3aaf2e8..ad162a7ef7 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -15,7 +15,12 @@ default = ["std", "async_executor"] ## Enables multi-threading support. ## Without this feature, all tasks will be run on a single thread. -multi_threaded = ["std", "dep:async-channel", "dep:concurrent-queue"] +multi_threaded = [ + "std", + "dep:async-channel", + "dep:concurrent-queue", + "async_executor", +] ## Uses `async-executor` as a task execution backend. ## This backend is incompatible with `no_std` targets. diff --git a/crates/bevy_text/src/bounds.rs b/crates/bevy_text/src/bounds.rs index db2ceb0b28..1c0833b443 100644 --- a/crates/bevy_text/src/bounds.rs +++ b/crates/bevy_text/src/bounds.rs @@ -5,7 +5,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; /// The maximum width and height of text. The text will wrap according to the specified size. /// /// Characters out of the bounds after wrapping will be truncated. Text is aligned according to the -/// specified [`JustifyText`](crate::text::JustifyText). +/// specified [`Justify`](crate::text::Justify). /// /// Note: only characters that are completely out of the bounds will be truncated, so this is not a /// reliable limit if it is necessary to contain the text strictly in the bounds. Currently this diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 2bc74a1aa7..b36f5fa2bb 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -61,7 +61,7 @@ pub use text_access::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - Font, JustifyText, LineBreak, Text2d, Text2dReader, Text2dWriter, TextColor, TextError, + Font, Justify, LineBreak, Text2d, Text2dReader, Text2dWriter, TextColor, TextError, TextFont, TextLayout, TextSpan, }; } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 50b983f929..dd6ca77246 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -16,8 +16,8 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; use crate::{ - error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, JustifyText, - LineBreak, PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, + error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, Justify, LineBreak, + PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, }; /// A wrapper resource around a [`cosmic_text::FontSystem`] @@ -88,7 +88,7 @@ impl TextPipeline { fonts: &Assets, text_spans: impl Iterator, linebreak: LineBreak, - justify: JustifyText, + justify: Justify, bounds: TextBounds, scale_factor: f64, computed: &mut ComputedTextBlock, @@ -201,7 +201,7 @@ impl TextPipeline { // Workaround for alignment not working for unbounded text. // See https://github.com/pop-os/cosmic-text/issues/343 - if bounds.width.is_none() && justify != JustifyText::Left { + if bounds.width.is_none() && justify != Justify::Left { let dimensions = buffer_dimensions(buffer); // `set_size` causes a re-layout to occur. buffer.set_size(font_system, Some(dimensions.x), bounds.height); diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index debf9cc375..ccfdb2a372 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -116,19 +116,19 @@ impl Default for ComputedTextBlock { pub struct TextLayout { /// The text's internal alignment. /// Should not affect its position within a container. - pub justify: JustifyText, + pub justify: Justify, /// How the text should linebreak when running out of the bounds determined by `max_size`. pub linebreak: LineBreak, } impl TextLayout { /// Makes a new [`TextLayout`]. - pub const fn new(justify: JustifyText, linebreak: LineBreak) -> Self { + pub const fn new(justify: Justify, linebreak: LineBreak) -> Self { Self { justify, linebreak } } - /// Makes a new [`TextLayout`] with the specified [`JustifyText`]. - pub fn new_with_justify(justify: JustifyText) -> Self { + /// Makes a new [`TextLayout`] with the specified [`Justify`]. + pub fn new_with_justify(justify: Justify) -> Self { Self::default().with_justify(justify) } @@ -143,8 +143,8 @@ impl TextLayout { Self::default().with_no_wrap() } - /// Returns this [`TextLayout`] with the specified [`JustifyText`]. - pub const fn with_justify(mut self, justify: JustifyText) -> Self { + /// Returns this [`TextLayout`] with the specified [`Justify`]. + pub const fn with_justify(mut self, justify: Justify) -> Self { self.justify = justify; self } @@ -246,7 +246,7 @@ impl From for TextSpan { /// [`TextBounds`](super::bounds::TextBounds) component with an explicit `width` value. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize, Clone, PartialEq, Hash)] -pub enum JustifyText { +pub enum Justify { /// Leftmost character is immediately to the right of the render position. /// Bounds start from the render position and advance rightwards. #[default] @@ -263,13 +263,13 @@ pub enum JustifyText { Justified, } -impl From for cosmic_text::Align { - fn from(justify: JustifyText) -> Self { +impl From for cosmic_text::Align { + fn from(justify: Justify) -> Self { match justify { - JustifyText::Left => cosmic_text::Align::Left, - JustifyText::Center => cosmic_text::Align::Center, - JustifyText::Right => cosmic_text::Align::Right, - JustifyText::Justified => cosmic_text::Align::Justified, + Justify::Left => cosmic_text::Align::Left, + Justify::Center => cosmic_text::Align::Center, + Justify::Right => cosmic_text::Align::Right, + Justify::Justified => cosmic_text::Align::Justified, } } } @@ -354,8 +354,8 @@ impl Default for TextFont { /// Specifies the height of each line of text for `Text` and `Text2d` /// /// Default is 1.2x the font size -#[derive(Debug, Clone, Copy, Reflect)] -#[reflect(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(Debug, Clone, PartialEq)] pub enum LineHeight { /// Set line height to a specific number of pixels Px(f32), diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 5069804df8..8d4a926e1b 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -51,7 +51,7 @@ use bevy_window::{PrimaryWindow, Window}; /// # use bevy_color::Color; /// # use bevy_color::palettes::basic::BLUE; /// # use bevy_ecs::world::World; -/// # use bevy_text::{Font, JustifyText, Text2d, TextLayout, TextFont, TextColor, TextSpan}; +/// # use bevy_text::{Font, Justify, Text2d, TextLayout, TextFont, TextColor, TextSpan}; /// # /// # let font_handle: Handle = Default::default(); /// # let mut world = World::default(); @@ -73,7 +73,7 @@ use bevy_window::{PrimaryWindow, Window}; /// // With text justification. /// world.spawn(( /// Text2d::new("hello world\nand bevy!"), -/// TextLayout::new_with_justify(JustifyText::Center) +/// TextLayout::new_with_justify(Justify::Center) /// )); /// /// // With spans diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs new file mode 100644 index 0000000000..f91cdaee59 --- /dev/null +++ b/crates/bevy_ui/src/interaction_states.rs @@ -0,0 +1,74 @@ +/// This module contains components that are used to track the interaction state of UI widgets. +use bevy_a11y::AccessibilityNode; +use bevy_ecs::{ + component::Component, + lifecycle::{OnAdd, OnInsert, OnRemove}, + observer::Trigger, + world::DeferredWorld, +}; + +/// A component indicating that a widget is disabled and should be "grayed out". +/// This is used to prevent user interaction with the widget. It should not, however, prevent +/// the widget from being updated or rendered, or from acquiring keyboard focus. +/// +/// For apps which support a11y: if a widget (such as a slider) contains multiple entities, +/// the `InteractionDisabled` component should be added to the root entity of the widget - the +/// same entity that contains the `AccessibilityNode` component. This will ensure that +/// the a11y tree is updated correctly. +#[derive(Component, Debug, Clone, Copy, Default)] +pub struct InteractionDisabled; + +pub(crate) fn on_add_disabled( + trigger: Trigger, + mut world: DeferredWorld, +) { + let mut entity = world.entity_mut(trigger.target().unwrap()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_disabled(); + } +} + +pub(crate) fn on_remove_disabled( + trigger: Trigger, + mut world: DeferredWorld, +) { + let mut entity = world.entity_mut(trigger.target().unwrap()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.clear_disabled(); + } +} + +/// Component that indicates whether a button or widget is currently in a pressed or "held down" +/// state. +#[derive(Component, Default, Debug)] +pub struct Pressed; + +/// Component that indicates whether a checkbox or radio button is in a checked state. +#[derive(Component, Default, Debug)] +#[component(immutable)] +pub struct Checked(pub bool); + +impl Checked { + /// Returns whether the checkbox or radio button is currently checked. + pub fn get(&self) -> bool { + self.0 + } +} + +pub(crate) fn on_insert_is_checked(trigger: Trigger, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target().unwrap()); + let checked = entity.get::().unwrap().get(); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(match checked { + true => accesskit::Toggled::True, + false => accesskit::Toggled::False, + }); + } +} + +pub(crate) fn on_remove_is_checked(trigger: Trigger, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target().unwrap()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(accesskit::Toggled::False); + } +} diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index a5e53d981e..484acbd4af 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -8,8 +8,8 @@ use bevy_ecs::{ change_detection::{DetectChanges, DetectChangesMut}, entity::Entity, hierarchy::{ChildOf, Children}, + lifecycle::RemovedComponents, query::With, - removal_detection::RemovedComponents, system::{Commands, Query, ResMut}, world::Ref, }; diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index be0649c038..ac70897d06 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -10,6 +10,7 @@ //! Spawn UI elements with [`widget::Button`], [`ImageNode`], [`Text`](prelude::Text) and [`Node`] //! This UI is laid out with the Flexbox and CSS Grid layout models (see ) +pub mod interaction_states; pub mod measurement; pub mod ui_material; pub mod update; @@ -38,6 +39,7 @@ mod ui_node; pub use focus::*; pub use geometry::*; pub use gradients::*; +pub use interaction_states::{Checked, InteractionDisabled, Pressed}; pub use layout::*; pub use measurement::*; pub use render::*; @@ -319,6 +321,11 @@ fn build_text_interop(app: &mut App) { app.add_plugins(accessibility::AccessibilityPlugin); + app.add_observer(interaction_states::on_add_disabled) + .add_observer(interaction_states::on_remove_disabled) + .add_observer(interaction_states::on_insert_is_checked) + .add_observer(interaction_states::on_remove_is_checked); + app.configure_sets( PostUpdate, AmbiguousWithText.ambiguous_with(widget::text_system), diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 343d8af3cd..1c5ed364b9 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -2130,6 +2130,16 @@ impl BorderColor { } } + /// Helper to set all border colors to a given color. + pub fn set_all(&mut self, color: impl Into) -> &mut Self { + let color: Color = color.into(); + self.top = color; + self.bottom = color; + self.left = color; + self.right = color; + self + } + /// Check if all contained border colors are transparent pub fn is_fully_transparent(&self) -> bool { self.top.is_fully_transparent() @@ -2865,8 +2875,8 @@ impl ComputedNodeTarget { } /// Adds a shadow behind text -#[derive(Component, Copy, Clone, Debug, Reflect)] -#[reflect(Component, Default, Debug, Clone)] +#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] +#[reflect(Component, Default, Debug, Clone, PartialEq)] pub struct TextShadow { /// Shadow displacement in logical pixels /// With a value of zero the shadow will be hidden directly behind the text diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index c65c4df354..9a743595b8 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -138,8 +138,8 @@ impl From> for ImageNode { } /// Controls how the image is altered to fit within the layout and how the layout algorithm determines the space in the layout for the image -#[derive(Default, Debug, Clone, Reflect)] -#[reflect(Clone, Default)] +#[derive(Default, Debug, Clone, PartialEq, Reflect)] +#[reflect(Clone, Default, PartialEq)] pub enum NodeImageMode { /// The image will be sized automatically by taking the size of the source image and applying any layout constraints. #[default] diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 785040c1e9..d7f8e243a4 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -61,7 +61,7 @@ impl Default for TextNodeFlags { /// # use bevy_color::Color; /// # use bevy_color::palettes::basic::BLUE; /// # use bevy_ecs::world::World; -/// # use bevy_text::{Font, JustifyText, TextLayout, TextFont, TextColor, TextSpan}; +/// # use bevy_text::{Font, Justify, TextLayout, TextFont, TextColor, TextSpan}; /// # use bevy_ui::prelude::Text; /// # /// # let font_handle: Handle = Default::default(); @@ -84,7 +84,7 @@ impl Default for TextNodeFlags { /// // With text justification. /// world.spawn(( /// Text::new("hello world\nand bevy!"), -/// TextLayout::new_with_justify(JustifyText::Center) +/// TextLayout::new_with_justify(Justify::Center) /// )); /// /// // With spans diff --git a/crates/bevy_winit/src/cursor.rs b/crates/bevy_winit/src/cursor.rs index 63c8a2bb2d..e3e4cb9f87 100644 --- a/crates/bevy_winit/src/cursor.rs +++ b/crates/bevy_winit/src/cursor.rs @@ -22,11 +22,12 @@ use bevy_ecs::{ change_detection::DetectChanges, component::Component, entity::Entity, + lifecycle::OnRemove, observer::Trigger, query::With, reflect::ReflectComponent, system::{Commands, Local, Query}, - world::{OnRemove, Ref}, + world::Ref, }; #[cfg(feature = "custom_cursor")] use bevy_image::{Image, TextureAtlasLayout}; diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 97483c7358..873949ea89 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -3,9 +3,9 @@ use std::collections::HashMap; use bevy_ecs::{ entity::Entity, event::EventWriter, + lifecycle::RemovedComponents, prelude::{Changed, Component}, query::QueryFilter, - removal_detection::RemovedComponents, system::{Local, NonSendMarker, Query, SystemParamItem}, }; use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput}; diff --git a/docs/cargo_features.md b/docs/cargo_features.md index e0f00f2f3d..8784d5e725 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -21,6 +21,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_audio|Provides audio functionality| |bevy_color|Provides shared color types and operations| |bevy_core_pipeline|Provides cameras and other basic render pipeline features| +|bevy_core_widgets|Headless widget collection for Bevy UI.| |bevy_gilrs|Adds gamepad support| |bevy_gizmos|Adds support for rendering gizmos| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support| diff --git a/examples/2d/sprite_scale.rs b/examples/2d/sprite_scale.rs index c549134419..9cffb8e00c 100644 --- a/examples/2d/sprite_scale.rs +++ b/examples/2d/sprite_scale.rs @@ -129,7 +129,7 @@ fn setup_sprites(mut commands: Commands, asset_server: Res) { cmd.with_children(|builder| { builder.spawn(( Text2d::new(rect.text), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * rect.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, @@ -275,7 +275,7 @@ fn setup_texture_atlas( cmd.with_children(|builder| { builder.spawn(( Text2d::new(sprite_sheet.text), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * sprite_sheet.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, diff --git a/examples/2d/sprite_slice.rs b/examples/2d/sprite_slice.rs index 94f4fe809f..91918b1d66 100644 --- a/examples/2d/sprite_slice.rs +++ b/examples/2d/sprite_slice.rs @@ -94,7 +94,7 @@ fn spawn_sprites( children![( Text2d::new(label), text_style, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_xyz(0., -0.5 * size.y - 10., 0.0), bevy::sprite::Anchor::TOP_CENTER, )], diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 7b1abfd8da..3123e8d891 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -40,7 +40,7 @@ fn setup(mut commands: Commands, asset_server: Res) { font_size: 50.0, ..default() }; - let text_justification = JustifyText::Center; + let text_justification = Justify::Center; commands.spawn(Camera2d); // Demonstrate changing translation commands.spawn(( @@ -78,7 +78,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(Unicode linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(JustifyText::Left, LineBreak::WordBoundary), + TextLayout::new(Justify::Left, LineBreak::WordBoundary), // Wrap text in the rectangle TextBounds::from(box_size), // Ensure the text is drawn on top of the box @@ -94,7 +94,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(AnyCharacter linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(JustifyText::Left, LineBreak::AnyCharacter), + TextLayout::new(Justify::Left, LineBreak::AnyCharacter), // Wrap text in the rectangle TextBounds::from(other_box_size), // Ensure the text is drawn on top of the box @@ -104,11 +104,11 @@ fn setup(mut commands: Commands, asset_server: Res) { // Demonstrate font smoothing off commands.spawn(( - Text2d::new("This text has\nFontSmoothing::None\nAnd JustifyText::Center"), + Text2d::new("This text has\nFontSmoothing::None\nAnd Justify::Center"), slightly_smaller_text_font .clone() .with_font_smoothing(FontSmoothing::None), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_translation(Vec3::new(-400.0, -250.0, 0.0)), )); diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index 7510afbcee..25106adcfb 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -279,7 +279,7 @@ fn create_label( commands.spawn(( Text2d::new(text), text_style, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform { translation: Vec3::new(translation.0, translation.1, translation.2), ..default() diff --git a/examples/3d/pbr.rs b/examples/3d/pbr.rs index 12922db03b..da654ce1f3 100644 --- a/examples/3d/pbr.rs +++ b/examples/3d/pbr.rs @@ -85,8 +85,8 @@ fn setup( right: Val::ZERO, ..default() }, - Transform { - rotation: Quat::from_rotation_z(std::f32::consts::PI / 2.0), + UiTransform { + rotation: Rot2::degrees(90.), ..default() }, )); diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index 0808776e2b..987ceda70d 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -180,7 +180,7 @@ fn setup_image_viewer_scene( ..default() }, TextColor(Color::BLACK), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Node { align_self: AlignSelf::Center, margin: UiRect::all(Val::Auto), diff --git a/examples/README.md b/examples/README.md index dce7c114e8..4e679d0d7a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -545,6 +545,8 @@ Example | Description [Box Shadow](../examples/ui/box_shadow.rs) | Demonstrates how to create a node with a shadow [Button](../examples/ui/button.rs) | Illustrates creating and updating a button [CSS Grid](../examples/ui/grid.rs) | An example for CSS Grid layout +[Core Widgets](../examples/ui/core_widgets.rs) | Demonstrates use of core (headless) widgets in Bevy UI +[Core Widgets (w/Observers)](../examples/ui/core_widgets_observers.rs) | Demonstrates use of core (headless) widgets in Bevy UI, with Observers [Directional Navigation](../examples/ui/directional_navigation.rs) | Demonstration of Directional Navigation between UI elements [Display and Visibility](../examples/ui/display_and_visibility.rs) | Demonstrates how Display and Visibility work in the UI. [Flex Layout](../examples/ui/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text diff --git a/examples/animation/animated_ui.rs b/examples/animation/animated_ui.rs index f31b2ccd5e..68b0eb7a8f 100644 --- a/examples/animation/animated_ui.rs +++ b/examples/animation/animated_ui.rs @@ -151,7 +151,7 @@ fn setup( ..default() }, TextColor(Color::Srgba(Srgba::RED)), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), )) // Mark as an animation target. .insert(AnimationTarget { diff --git a/examples/animation/animation_graph.rs b/examples/animation/animation_graph.rs index 610074744f..884ec1a2af 100644 --- a/examples/animation/animation_graph.rs +++ b/examples/animation/animation_graph.rs @@ -277,7 +277,7 @@ fn setup_node_rects(commands: &mut Commands) { ..default() }, TextColor(ANTIQUE_WHITE.into()), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), )) .id(); diff --git a/examples/animation/animation_masks.rs b/examples/animation/animation_masks.rs index 07261b40df..05b50711fe 100644 --- a/examples/animation/animation_masks.rs +++ b/examples/animation/animation_masks.rs @@ -334,7 +334,7 @@ fn add_mask_group_control( } else { selected_button_text_style.clone() }, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Node { flex_grow: 1.0, margin: UiRect::vertical(Val::Px(3.0)), diff --git a/examples/async_tasks/external_source_external_thread.rs b/examples/async_tasks/external_source_external_thread.rs index 1b7bb27b16..1b437ed76c 100644 --- a/examples/async_tasks/external_source_external_thread.rs +++ b/examples/async_tasks/external_source_external_thread.rs @@ -54,7 +54,7 @@ fn spawn_text(mut commands: Commands, mut reader: EventReader) { for (per_frame, event) in reader.read().enumerate() { commands.spawn(( Text2d::new(event.0.to_string()), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_xyz(per_frame as f32 * 100.0, 300.0, 0.0), )); } diff --git a/examples/ecs/component_hooks.rs b/examples/ecs/component_hooks.rs index 11c469ad15..4ce6b7b09b 100644 --- a/examples/ecs/component_hooks.rs +++ b/examples/ecs/component_hooks.rs @@ -14,7 +14,8 @@ //! between components (like hierarchies or parent-child links) and need to maintain correctness. use bevy::{ - ecs::component::{ComponentHook, HookContext, Mutable, StorageType}, + ecs::component::{Mutable, StorageType}, + ecs::lifecycle::{ComponentHook, HookContext}, prelude::*, }; use std::collections::HashMap; diff --git a/examples/ecs/immutable_components.rs b/examples/ecs/immutable_components.rs index 5f4f2db8c4..e9d3c3b383 100644 --- a/examples/ecs/immutable_components.rs +++ b/examples/ecs/immutable_components.rs @@ -2,9 +2,8 @@ use bevy::{ ecs::{ - component::{ - ComponentCloneBehavior, ComponentDescriptor, ComponentId, HookContext, StorageType, - }, + component::{ComponentCloneBehavior, ComponentDescriptor, ComponentId, StorageType}, + lifecycle::HookContext, world::DeferredWorld, }, platform::collections::HashMap, diff --git a/examples/ecs/one_shot_systems.rs b/examples/ecs/one_shot_systems.rs index 8dfb90f175..faa40b6184 100644 --- a/examples/ecs/one_shot_systems.rs +++ b/examples/ecs/one_shot_systems.rs @@ -94,7 +94,7 @@ fn setup_ui(mut commands: Commands) { commands .spawn(( Text::default(), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Node { align_self: AlignSelf::Center, justify_self: JustifySelf::Center, diff --git a/examples/helpers/camera_controller.rs b/examples/helpers/camera_controller.rs index 07f0f31b11..a60aa69a5e 100644 --- a/examples/helpers/camera_controller.rs +++ b/examples/helpers/camera_controller.rs @@ -198,7 +198,7 @@ fn run_camera_controller( } let cursor_grab = *mouse_cursor_grab || *toggle_cursor_grab; - // Apply movement update + // Update velocity if axis_input != Vec3::ZERO { let max_speed = if key_input.pressed(controller.key_run) { controller.run_speed @@ -213,11 +213,15 @@ fn run_camera_controller( controller.velocity = Vec3::ZERO; } } - let forward = *transform.forward(); - let right = *transform.right(); - transform.translation += controller.velocity.x * dt * right - + controller.velocity.y * dt * Vec3::Y - + controller.velocity.z * dt * forward; + + // Apply movement update + if controller.velocity != Vec3::ZERO { + let forward = *transform.forward(); + let right = *transform.right(); + transform.translation += controller.velocity.x * dt * right + + controller.velocity.y * dt * Vec3::Y + + controller.velocity.z * dt * forward; + } // Handle cursor grab if cursor_grab_change { diff --git a/examples/math/render_primitives.rs b/examples/math/render_primitives.rs index 7d1e3aca97..26be0445ba 100644 --- a/examples/math/render_primitives.rs +++ b/examples/math/render_primitives.rs @@ -379,7 +379,7 @@ fn setup_text(mut commands: Commands, cameras: Query<(Entity, &Camera)>) { children![( Text::default(), HeaderText, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), children![ TextSpan::new("Primitive: "), TextSpan(format!("{text}", text = PrimitiveSelected::default())), diff --git a/examples/mobile/src/lib.rs b/examples/mobile/src/lib.rs index ba93268e86..de83152552 100644 --- a/examples/mobile/src/lib.rs +++ b/examples/mobile/src/lib.rs @@ -158,7 +158,7 @@ fn setup_scene( ..default() }, TextColor::BLACK, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), )); } diff --git a/examples/picking/mesh_picking.rs b/examples/picking/mesh_picking.rs index 4c247aa62a..c9023f48b8 100644 --- a/examples/picking/mesh_picking.rs +++ b/examples/picking/mesh_picking.rs @@ -91,8 +91,8 @@ fn setup_scene( )) .observe(update_material_on::>(hover_matl.clone())) .observe(update_material_on::>(white_matl.clone())) - .observe(update_material_on::>(pressed_matl.clone())) - .observe(update_material_on::>(hover_matl.clone())) + .observe(update_material_on::>(pressed_matl.clone())) + .observe(update_material_on::>(hover_matl.clone())) .observe(rotate_on_drag); } @@ -114,8 +114,8 @@ fn setup_scene( )) .observe(update_material_on::>(hover_matl.clone())) .observe(update_material_on::>(white_matl.clone())) - .observe(update_material_on::>(pressed_matl.clone())) - .observe(update_material_on::>(hover_matl.clone())) + .observe(update_material_on::>(pressed_matl.clone())) + .observe(update_material_on::>(hover_matl.clone())) .observe(rotate_on_drag); } diff --git a/examples/picking/sprite_picking.rs b/examples/picking/sprite_picking.rs index 99403e9192..cf7308671e 100644 --- a/examples/picking/sprite_picking.rs +++ b/examples/picking/sprite_picking.rs @@ -63,8 +63,8 @@ fn setup(mut commands: Commands, asset_server: Res) { )) .observe(recolor_on::>(Color::srgb(0.0, 1.0, 1.0))) .observe(recolor_on::>(Color::BLACK)) - .observe(recolor_on::>(Color::srgb(1.0, 1.0, 0.0))) - .observe(recolor_on::>(Color::srgb(0.0, 1.0, 1.0))); + .observe(recolor_on::>(Color::srgb(1.0, 1.0, 0.0))) + .observe(recolor_on::>(Color::srgb(0.0, 1.0, 1.0))); commands .spawn(( @@ -83,8 +83,8 @@ fn setup(mut commands: Commands, asset_server: Res) { )) .observe(recolor_on::>(Color::srgb(0.0, 1.0, 0.0))) .observe(recolor_on::>(Color::srgb(1.0, 0.0, 0.0))) - .observe(recolor_on::>(Color::srgb(0.0, 0.0, 1.0))) - .observe(recolor_on::>(Color::srgb(0.0, 1.0, 0.0))); + .observe(recolor_on::>(Color::srgb(0.0, 0.0, 1.0))) + .observe(recolor_on::>(Color::srgb(0.0, 1.0, 0.0))); } }); } @@ -145,8 +145,8 @@ fn setup_atlas( )) .observe(recolor_on::>(Color::srgb(0.0, 1.0, 1.0))) .observe(recolor_on::>(Color::srgb(1.0, 1.0, 1.0))) - .observe(recolor_on::>(Color::srgb(1.0, 1.0, 0.0))) - .observe(recolor_on::>(Color::srgb(0.0, 1.0, 1.0))); + .observe(recolor_on::>(Color::srgb(1.0, 1.0, 0.0))) + .observe(recolor_on::>(Color::srgb(0.0, 1.0, 1.0))); } // An observer listener that changes the target entity's color. diff --git a/examples/stress_tests/many_cameras_lights.rs b/examples/stress_tests/many_cameras_lights.rs index 6d3f5f24d0..3f9c4878e0 100644 --- a/examples/stress_tests/many_cameras_lights.rs +++ b/examples/stress_tests/many_cameras_lights.rs @@ -6,11 +6,19 @@ use bevy::{ math::ops::{cos, sin}, prelude::*, render::camera::Viewport, + window::{PresentMode, WindowResolution}, }; fn main() { App::new() - .add_plugins(DefaultPlugins) + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + present_mode: PresentMode::AutoNoVsync, + resolution: WindowResolution::new(1920.0, 1080.0).with_scale_factor_override(1.0), + ..default() + }), + ..default() + })) .add_systems(Startup, setup) .add_systems(Update, rotate_cameras) .run(); diff --git a/examples/stress_tests/many_glyphs.rs b/examples/stress_tests/many_glyphs.rs index d6629fa80c..dc94e65ca8 100644 --- a/examples/stress_tests/many_glyphs.rs +++ b/examples/stress_tests/many_glyphs.rs @@ -74,7 +74,7 @@ fn setup(mut commands: Commands, args: Res) { ..Default::default() }; let text_block = TextLayout { - justify: JustifyText::Left, + justify: Justify::Left, linebreak: LineBreak::AnyCharacter, }; diff --git a/examples/stress_tests/many_text2d.rs b/examples/stress_tests/many_text2d.rs index c05ce00a47..cc1247e13f 100644 --- a/examples/stress_tests/many_text2d.rs +++ b/examples/stress_tests/many_text2d.rs @@ -47,7 +47,7 @@ struct Args { #[argh(switch)] no_frustum_culling: bool, - /// whether the text should use `JustifyText::Center`. + /// whether the text should use `Justify::Center`. #[argh(switch)] center: bool, } @@ -132,9 +132,9 @@ fn setup(mut commands: Commands, font: Res, args: Res) { random_text_font(&mut rng, &args, font.0.clone()), TextColor(color.into()), TextLayout::new_with_justify(if args.center { - JustifyText::Center + Justify::Center } else { - JustifyText::Left + Justify::Left }), Transform { translation, diff --git a/examples/stress_tests/text_pipeline.rs b/examples/stress_tests/text_pipeline.rs index acb0b0e804..5f9fdca8e0 100644 --- a/examples/stress_tests/text_pipeline.rs +++ b/examples/stress_tests/text_pipeline.rs @@ -69,7 +69,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { .spawn(( Text2d::default(), TextLayout { - justify: JustifyText::Center, + justify: Justify::Center, linebreak: LineBreak::AnyCharacter, }, TextBounds::default(), diff --git a/examples/testbed/2d.rs b/examples/testbed/2d.rs index c812157899..4d53daf507 100644 --- a/examples/testbed/2d.rs +++ b/examples/testbed/2d.rs @@ -151,10 +151,10 @@ mod text { commands.spawn((Camera2d, DespawnOnExitState(super::Scene::Text))); for (i, justify) in [ - JustifyText::Left, - JustifyText::Right, - JustifyText::Center, - JustifyText::Justified, + Justify::Left, + Justify::Right, + Justify::Center, + Justify::Justified, ] .into_iter() .enumerate() @@ -196,7 +196,7 @@ mod text { fn spawn_anchored_text( commands: &mut Commands, dest: Vec3, - justify: JustifyText, + justify: Justify, bounds: Option, ) { commands.spawn(( diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index eba2afb409..6538840575 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -372,7 +372,7 @@ mod text_wrap { for (j, message) in messages.into_iter().enumerate() { commands.entity(root).with_child(( Text(message.clone()), - TextLayout::new(JustifyText::Left, linebreak), + TextLayout::new(Justify::Left, linebreak), BackgroundColor(Color::srgb(0.8 - j as f32 * 0.3, 0., 0.)), )); } diff --git a/examples/time/virtual_time.rs b/examples/time/virtual_time.rs index 09923da7b7..4b6aec436e 100644 --- a/examples/time/virtual_time.rs +++ b/examples/time/virtual_time.rs @@ -102,7 +102,7 @@ fn setup(mut commands: Commands, asset_server: Res, mut time: ResMu ..default() }, TextColor(Color::srgb(0.85, 0.85, 0.85)), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), ), ( Text::default(), @@ -111,7 +111,7 @@ fn setup(mut commands: Commands, asset_server: Res, mut time: ResMu ..default() }, TextColor(virtual_color), - TextLayout::new_with_justify(JustifyText::Right), + TextLayout::new_with_justify(Justify::Right), VirtualTime, ), ], diff --git a/examples/ui/core_widgets.rs b/examples/ui/core_widgets.rs new file mode 100644 index 0000000000..594f43efd8 --- /dev/null +++ b/examples/ui/core_widgets.rs @@ -0,0 +1,233 @@ +//! This example illustrates how to create widgets using the `bevy_core_widgets` widget set. + +use bevy::{ + color::palettes::basic::*, + core_widgets::{CoreButton, CoreWidgetsPlugin}, + ecs::system::SystemId, + input_focus::{ + tab_navigation::{TabGroup, TabIndex}, + InputDispatchPlugin, + }, + picking::hover::Hovered, + prelude::*, + ui::{InteractionDisabled, Pressed}, + winit::WinitSettings, +}; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin)) + // Only run the app when there is user input. This will significantly reduce CPU/GPU use. + .insert_resource(WinitSettings::desktop_app()) + .add_systems(Startup, setup) + .add_systems( + Update, + (update_button_style, update_button_style2, toggle_disabled), + ) + .run(); +} + +const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); +const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); +const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); + +/// Marker which identifies buttons with a particular style, in this case the "Demo style". +#[derive(Component)] +struct DemoButton; + +fn update_button_style( + mut buttons: Query< + ( + Has, + &Hovered, + Has, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + ( + Or<( + Changed, + Changed, + Added, + )>, + With, + ), + >, + mut text_query: Query<&mut Text>, +) { + for (pressed, hovered, disabled, mut color, mut border_color, children) in &mut buttons { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + disabled, + hovered.get(), + pressed, + &mut color, + &mut border_color, + &mut text, + ); + } +} + +/// Supplementary system to detect removed marker components +fn update_button_style2( + mut buttons: Query< + ( + Has, + &Hovered, + Has, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + With, + >, + mut removed_depressed: RemovedComponents, + mut removed_disabled: RemovedComponents, + mut text_query: Query<&mut Text>, +) { + removed_depressed.read().for_each(|entity| { + if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) = + buttons.get_mut(entity) + { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + disabled, + hovered.get(), + pressed, + &mut color, + &mut border_color, + &mut text, + ); + } + }); + removed_disabled.read().for_each(|entity| { + if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) = + buttons.get_mut(entity) + { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + disabled, + hovered.get(), + pressed, + &mut color, + &mut border_color, + &mut text, + ); + } + }); +} + +fn set_button_style( + disabled: bool, + hovered: bool, + pressed: bool, + color: &mut BackgroundColor, + border_color: &mut BorderColor, + text: &mut Text, +) { + match (disabled, hovered, pressed) { + // Disabled button + (true, _, _) => { + **text = "Disabled".to_string(); + *color = NORMAL_BUTTON.into(); + border_color.set_all(GRAY); + } + + // Pressed and hovered button + (false, true, true) => { + **text = "Press".to_string(); + *color = PRESSED_BUTTON.into(); + border_color.set_all(RED); + } + + // Hovered, unpressed button + (false, true, false) => { + **text = "Hover".to_string(); + *color = HOVERED_BUTTON.into(); + border_color.set_all(WHITE); + } + + // Unhovered button (either pressed or not). + (false, false, _) => { + **text = "Button".to_string(); + *color = NORMAL_BUTTON.into(); + border_color.set_all(BLACK); + } + } +} + +fn setup(mut commands: Commands, assets: Res) { + let on_click = commands.register_system(|| { + info!("Button clicked!"); + }); + // ui camera + commands.spawn(Camera2d); + commands.spawn(button(&assets, on_click)); +} + +fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { + ( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + display: Display::Flex, + flex_direction: FlexDirection::Column, + ..default() + }, + TabGroup::default(), + children![ + ( + Node { + width: Val::Px(150.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + DemoButton, + CoreButton { + on_click: Some(on_click), + }, + Hovered::default(), + TabIndex(0), + BorderColor::all(Color::BLACK), + BorderRadius::MAX, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Button"), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + TextShadow::default(), + )] + ), + Text::new("Press 'D' to toggle button disabled state"), + ], + ) +} + +fn toggle_disabled( + input: Res>, + mut interaction_query: Query<(Entity, Has), With>, + mut commands: Commands, +) { + if input.just_pressed(KeyCode::KeyD) { + for (entity, disabled) in &mut interaction_query { + // disabled.0 = !disabled.0; + if disabled { + info!("Button enabled"); + commands.entity(entity).remove::(); + } else { + info!("Button disabled"); + commands.entity(entity).insert(InteractionDisabled); + } + } + } +} diff --git a/examples/ui/core_widgets_observers.rs b/examples/ui/core_widgets_observers.rs new file mode 100644 index 0000000000..206c596e2c --- /dev/null +++ b/examples/ui/core_widgets_observers.rs @@ -0,0 +1,303 @@ +//! This example illustrates how to create widgets using the `bevy_core_widgets` widget set. + +use bevy::{ + color::palettes::basic::*, + core_widgets::{CoreButton, CoreWidgetsPlugin}, + ecs::system::SystemId, + input_focus::{ + tab_navigation::{TabGroup, TabIndex}, + InputDispatchPlugin, + }, + picking::hover::Hovered, + prelude::*, + ui::{InteractionDisabled, Pressed}, + winit::WinitSettings, +}; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin)) + // Only run the app when there is user input. This will significantly reduce CPU/GPU use. + .insert_resource(WinitSettings::desktop_app()) + .add_systems(Startup, setup) + .add_observer(on_add_pressed) + .add_observer(on_remove_pressed) + .add_observer(on_add_disabled) + .add_observer(on_remove_disabled) + .add_observer(on_change_hover) + .add_systems(Update, toggle_disabled) + .run(); +} + +const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); +const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); +const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); + +/// Marker which identifies buttons with a particular style, in this case the "Demo style". +#[derive(Component)] +struct DemoButton; + +fn on_add_pressed( + trigger: Trigger, + mut buttons: Query< + ( + &Hovered, + Has, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + With, + >, + mut text_query: Query<&mut Text>, +) { + if let Ok((hovered, disabled, mut color, mut border_color, children)) = + buttons.get_mut(trigger.target().unwrap()) + { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + disabled, + hovered.get(), + true, + &mut color, + &mut border_color, + &mut text, + ); + } +} + +fn on_remove_pressed( + trigger: Trigger, + mut buttons: Query< + ( + &Hovered, + Has, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + With, + >, + mut text_query: Query<&mut Text>, +) { + if let Ok((hovered, disabled, mut color, mut border_color, children)) = + buttons.get_mut(trigger.target().unwrap()) + { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + disabled, + hovered.get(), + false, + &mut color, + &mut border_color, + &mut text, + ); + } +} + +fn on_add_disabled( + trigger: Trigger, + mut buttons: Query< + ( + Has, + &Hovered, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + With, + >, + mut text_query: Query<&mut Text>, +) { + if let Ok((pressed, hovered, mut color, mut border_color, children)) = + buttons.get_mut(trigger.target().unwrap()) + { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + true, + hovered.get(), + pressed, + &mut color, + &mut border_color, + &mut text, + ); + } +} + +fn on_remove_disabled( + trigger: Trigger, + mut buttons: Query< + ( + Has, + &Hovered, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + With, + >, + mut text_query: Query<&mut Text>, +) { + if let Ok((pressed, hovered, mut color, mut border_color, children)) = + buttons.get_mut(trigger.target().unwrap()) + { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + false, + hovered.get(), + pressed, + &mut color, + &mut border_color, + &mut text, + ); + } +} + +fn on_change_hover( + trigger: Trigger, + mut buttons: Query< + ( + Has, + &Hovered, + Has, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + With, + >, + mut text_query: Query<&mut Text>, +) { + if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) = + buttons.get_mut(trigger.target().unwrap()) + { + if children.is_empty() { + return; + } + let Ok(mut text) = text_query.get_mut(children[0]) else { + return; + }; + set_button_style( + disabled, + hovered.get(), + pressed, + &mut color, + &mut border_color, + &mut text, + ); + } +} + +fn set_button_style( + disabled: bool, + hovered: bool, + pressed: bool, + color: &mut BackgroundColor, + border_color: &mut BorderColor, + text: &mut Text, +) { + match (disabled, hovered, pressed) { + // Disabled button + (true, _, _) => { + **text = "Disabled".to_string(); + *color = NORMAL_BUTTON.into(); + border_color.set_all(GRAY); + } + + // Pressed and hovered button + (false, true, true) => { + **text = "Press".to_string(); + *color = PRESSED_BUTTON.into(); + border_color.set_all(RED); + } + + // Hovered, unpressed button + (false, true, false) => { + **text = "Hover".to_string(); + *color = HOVERED_BUTTON.into(); + border_color.set_all(WHITE); + } + + // Unhovered button (either pressed or not). + (false, false, _) => { + **text = "Button".to_string(); + *color = NORMAL_BUTTON.into(); + border_color.set_all(BLACK); + } + } +} + +fn setup(mut commands: Commands, assets: Res) { + let on_click = commands.register_system(|| { + info!("Button clicked!"); + }); + // ui camera + commands.spawn(Camera2d); + commands.spawn(button(&assets, on_click)); +} + +fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { + ( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + display: Display::Flex, + flex_direction: FlexDirection::Column, + ..default() + }, + TabGroup::default(), + children![ + ( + Node { + width: Val::Px(150.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + DemoButton, + CoreButton { + on_click: Some(on_click), + }, + Hovered::default(), + TabIndex(0), + BorderColor::all(Color::BLACK), + BorderRadius::MAX, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Button"), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + TextShadow::default(), + )] + ), + Text::new("Press 'D' to toggle button disabled state"), + ], + ) +} + +fn toggle_disabled( + input: Res>, + mut interaction_query: Query<(Entity, Has), With>, + mut commands: Commands, +) { + if input.just_pressed(KeyCode::KeyD) { + for (entity, disabled) in &mut interaction_query { + // disabled.0 = !disabled.0; + if disabled { + info!("Button enabled"); + commands.entity(entity).remove::(); + } else { + info!("Button disabled"); + commands.entity(entity).insert(InteractionDisabled); + } + } + } +} diff --git a/examples/ui/directional_navigation.rs b/examples/ui/directional_navigation.rs index a570846fa7..05b3effbc3 100644 --- a/examples/ui/directional_navigation.rs +++ b/examples/ui/directional_navigation.rs @@ -186,7 +186,7 @@ fn setup_ui( Text::new(button_name), // And center the text if it flows onto multiple lines TextLayout { - justify: JustifyText::Center, + justify: Justify::Center, ..default() }, )) diff --git a/examples/ui/display_and_visibility.rs b/examples/ui/display_and_visibility.rs index 239b9814db..3513c94321 100644 --- a/examples/ui/display_and_visibility.rs +++ b/examples/ui/display_and_visibility.rs @@ -99,7 +99,7 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(( Text::new("Use the panel on the right to change the Display and Visibility properties for the respective nodes of the panel on the left"), text_font.clone(), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Node { margin: UiRect::bottom(Val::Px(10.)), ..Default::default() @@ -153,13 +153,13 @@ fn setup(mut commands: Commands, asset_server: Res) { Text::new("Display::None\nVisibility::Hidden\nVisibility::Inherited"), text_font.clone(), TextColor(HIDDEN_COLOR), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), )); builder.spawn(( Text::new("-\n-\n-"), text_font.clone(), TextColor(DARK_GRAY.into()), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), )); builder.spawn((Text::new("The UI Node and its descendants will not be visible and will not be allotted any space in the UI layout.\nThe UI Node will not be visible but will still occupy space in the UI layout.\nThe UI node will inherit the visibility property of its parent. If it has no parent it will be visible."), text_font)); }); @@ -396,7 +396,7 @@ where builder.spawn(( Text(format!("{}::{:?}", Target::::NAME, T::default())), text_font, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), )); }); } diff --git a/examples/ui/scroll.rs b/examples/ui/scroll.rs index 535e351c16..a3a443b991 100644 --- a/examples/ui/scroll.rs +++ b/examples/ui/scroll.rs @@ -89,7 +89,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }) .observe(| - trigger: Trigger>, + trigger: Trigger>, mut commands: Commands | { if trigger.event().button == PointerButton::Primary { diff --git a/examples/ui/size_constraints.rs b/examples/ui/size_constraints.rs index 72a072d788..e69ec911fa 100644 --- a/examples/ui/size_constraints.rs +++ b/examples/ui/size_constraints.rs @@ -251,7 +251,7 @@ fn spawn_button( } else { UNHOVERED_TEXT_COLOR }), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), )); }); } diff --git a/examples/ui/text.rs b/examples/ui/text.rs index 8bf34cc96e..12ed5c53ae 100644 --- a/examples/ui/text.rs +++ b/examples/ui/text.rs @@ -40,7 +40,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, TextShadow::default(), // Set the justification of the Text - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), // Set the style of the Node itself. Node { position_type: PositionType::Absolute, diff --git a/examples/ui/text_background_colors.rs b/examples/ui/text_background_colors.rs index caf0b60e85..3f82190f74 100644 --- a/examples/ui/text_background_colors.rs +++ b/examples/ui/text_background_colors.rs @@ -43,7 +43,7 @@ fn setup(mut commands: Commands) { .spawn(( Text::default(), TextLayout { - justify: JustifyText::Center, + justify: Justify::Center, ..Default::default() }, )) diff --git a/examples/ui/text_debug.rs b/examples/ui/text_debug.rs index 10ba7040b7..948cf0a534 100644 --- a/examples/ui/text_debug.rs +++ b/examples/ui/text_debug.rs @@ -64,7 +64,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { )); builder.spawn(( Text::new( - "This text is right-justified. The `JustifyText` component controls the horizontal alignment of the lines of multi-line text relative to each other, and does not affect the text node's position in the UI layout.", + "This text is right-justified. The `Justify` component controls the horizontal alignment of the lines of multi-line text relative to each other, and does not affect the text node's position in the UI layout.", ), TextFont { font: font.clone(), @@ -72,7 +72,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { ..default() }, TextColor(YELLOW.into()), - TextLayout::new_with_justify(JustifyText::Right), + TextLayout::new_with_justify(Justify::Right), Node { max_width: Val::Px(300.), ..default() @@ -114,7 +114,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { ..default() }, TextColor(Color::srgb(0.8, 0.2, 0.7)), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Node { max_width: Val::Px(400.), ..default() @@ -130,7 +130,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { ..default() }, TextColor(YELLOW.into()), - TextLayout::new_with_justify(JustifyText::Left), + TextLayout::new_with_justify(Justify::Left), Node { max_width: Val::Px(300.), ..default() @@ -145,7 +145,7 @@ fn infotext_system(mut commands: Commands, asset_server: Res) { font_size: 29.0, ..default() }, - TextLayout::new_with_justify(JustifyText::Justified), + TextLayout::new_with_justify(Justify::Justified), TextColor(GREEN_YELLOW.into()), Node { max_width: Val::Px(300.), diff --git a/examples/ui/text_wrap_debug.rs b/examples/ui/text_wrap_debug.rs index 227fb15116..986d9c79ff 100644 --- a/examples/ui/text_wrap_debug.rs +++ b/examples/ui/text_wrap_debug.rs @@ -117,7 +117,7 @@ fn spawn(mut commands: Commands, asset_server: Res) { commands.entity(column_id).with_child(( Text(message.clone()), text_font.clone(), - TextLayout::new(JustifyText::Left, linebreak), + TextLayout::new(Justify::Left, linebreak), BackgroundColor(Color::srgb(0.8 - j as f32 * 0.2, 0., 0.)), )); } diff --git a/examples/usages/context_menu.rs b/examples/usages/context_menu.rs index ea595be877..d315f8badb 100644 --- a/examples/usages/context_menu.rs +++ b/examples/usages/context_menu.rs @@ -62,7 +62,7 @@ fn setup(mut commands: Commands) { commands.spawn(background_and_button()).observe( // any click bubbling up here should lead to closing any open menu - |_: Trigger>, mut commands: Commands| { + |_: Trigger>, mut commands: Commands| { commands.trigger(CloseContextMenus); }, ); @@ -108,11 +108,11 @@ fn on_trigger_menu(trigger: Trigger, mut commands: Commands) { ], )) .observe( - |trigger: Trigger>, + |trigger: Trigger>, menu_items: Query<&ContextMenuItem>, mut clear_col: ResMut, mut commands: Commands| { - // Note that we want to know the target of the `Pointer` event (Button) here. + // Note that we want to know the target of the `Pointer` event (Button) here. // Not to be confused with the trigger `target` let target = trigger.event().target; @@ -184,7 +184,7 @@ fn background_and_button() -> impl Bundle + use<> { )], )) .observe( - |mut trigger: Trigger>, mut commands: Commands| { + |mut trigger: Trigger>, mut commands: Commands| { // by default this event would bubble up further leading to the `CloseContextMenus` // event being triggered and undoing the opening of one here right away. trigger.propagate(false); diff --git a/release-content/migration-guides/component-lifecycle-module.md b/release-content/migration-guides/component-lifecycle-module.md new file mode 100644 index 0000000000..830ab351ac --- /dev/null +++ b/release-content/migration-guides/component-lifecycle-module.md @@ -0,0 +1,14 @@ +--- +title: Component lifecycle reorganization +pull_requests: [19543] +--- + +To improve documentation, discoverability and internal organization, we've gathered all of the component lifecycle-related code we could and moved it into a dedicated `lifecycle` module. + +The lifecycle / observer types (`OnAdd`, `OnInsert`, `OnRemove`, `OnReplace`, `OnDespawn`) have been moved from the `bevy_ecs::world` to `bevy_ecs::lifecycle`. + +The same move has been done for the more internal (but public) `ComponentId` constants: `ON_ADD`, `ON_INSERT`, `ON_REMOVE`, `ON_REPLACE`, `ON_DESPAWN`. + +The code for hooks (`HookContext`, `ComponentHook`, `ComponentHooks`) has been extracted from the very long `bevy_ecs::components` module, and now lives in the `bevy_ecs::lifecycle` module. + +The `RemovedComponents` `SystemParam`, along with the public `RemovedIter`, `RemovedIterWithId` and `RemovedComponentEvents` have also been moved into this module as they serve a similar role. All references to `bevy_ecs::removal_detection` can be replaced with `bevy_ecs::lifecycle`. diff --git a/release-content/migration-guides/component_entry.md b/release-content/migration-guides/component_entry.md new file mode 100644 index 0000000000..56b3b96da0 --- /dev/null +++ b/release-content/migration-guides/component_entry.md @@ -0,0 +1,8 @@ +--- +title: `Entry` enum is now `ComponentEntry` +pull_requests: [TODO] +--- + +The `Entry` enum in `bevy::ecs::world` has been renamed to `ComponentEntry`, to avoid name clashes with `hash_map`, `hash_table` and `hash_set` `Entry` types. + +Correspondingly, the nested `OccupiedEntry` and `VacantEntry` enums have been renamed to `OccupiedComponentEntry` and `VacantComponentEntry`. diff --git a/release-content/migration-guides/delete_component_access.md b/release-content/migration-guides/delete_component_access.md new file mode 100644 index 0000000000..5369c506c5 --- /dev/null +++ b/release-content/migration-guides/delete_component_access.md @@ -0,0 +1,10 @@ +--- +title: `System::component_access` has been deleted. +pull_requests: [19496] +--- + +`System::component_access` has been deleted. If you were calling this method, you can simply use +`my_system.component_access_set().combined_access()` to get the same result. + +If you were manually implementing this, it should be equivalent to `System::component_access_set` +anyway. diff --git a/release-content/migration-guides/entity_representation.md b/release-content/migration-guides/entity_representation.md index e042f493da..142b456b75 100644 --- a/release-content/migration-guides/entity_representation.md +++ b/release-content/migration-guides/entity_representation.md @@ -49,7 +49,7 @@ This means that where `Result` was returned, `Option` is It is well documented that both the bit format, serialization, and `Ord` implementations for `Entity` are subject to change between versions. Those have all changed in this version. -For entity ordering, the order still prioretizes an entity's generation, but after that, it now considers higher index entities less than lower index entities. +For entity ordering, the order still prioritizes an entity's generation, but after that, it now considers higher index entities less than lower index entities. The changes to serialization and the bit format are directly related. Effectively, this means that all serialized and transmuted entities will not work as expected and may crash. diff --git a/release-content/migration-guides/remove_deprecated_batch_spawning.md b/release-content/migration-guides/remove_deprecated_batch_spawning.md index 9ab5ab0bbf..1b17eba563 100644 --- a/release-content/migration-guides/remove_deprecated_batch_spawning.md +++ b/release-content/migration-guides/remove_deprecated_batch_spawning.md @@ -14,6 +14,6 @@ They were deprecated in 0.16 for being unnecessary with the retained render worl Instead of these functions consider doing one of the following: -Option A) Instead of despawing entities, insert the `Disabled` component, and instead of respawning them at particular ids, use `try_insert_batch` or `insert_batch` and remove `Disabled`. +Option A) Instead of despawning entities, insert the `Disabled` component, and instead of respawning them at particular ids, use `try_insert_batch` or `insert_batch` and remove `Disabled`. Option B) Instead of giving special meaning to an entity id, simply use `spawn_batch` and ensure entity references are valid when despawning. diff --git a/release-content/migration-guides/rename-justifytext.md b/release-content/migration-guides/rename-justifytext.md new file mode 100644 index 0000000000..04b351a21f --- /dev/null +++ b/release-content/migration-guides/rename-justifytext.md @@ -0,0 +1,8 @@ +--- +title: Renamed `JustifyText` to `Justify` +pull_requests: [19522] +--- + +`JustifyText` has been renamed to `Justify`. + +The `-Text` suffix was inconsistent with the names of other `bevy_text` types and unnecessary since it's natural to assume `Justify` refers to text justification. diff --git a/release-content/migration-guides/rename_pointer_events.md b/release-content/migration-guides/rename_pointer_events.md new file mode 100644 index 0000000000..38e9b07821 --- /dev/null +++ b/release-content/migration-guides/rename_pointer_events.md @@ -0,0 +1,6 @@ +--- +title: Rename `Pointer` and `Pointer` to `Pointer` and `Pointer` +pull_requests: [19179] +--- + +The `Pointer` and `Pointer` events have been renamed to `Pointer` and `Pointer` for improved consistency. `Pressed` is now a marker component indicating that a button or other UI node is in a pressed or "held down" state. diff --git a/release-content/release-notes/headless-widgets.md b/release-content/release-notes/headless-widgets.md new file mode 100644 index 0000000000..5d39ffe755 --- /dev/null +++ b/release-content/release-notes/headless-widgets.md @@ -0,0 +1,90 @@ +--- +title: Headless Widgets +authors: ["@viridia"] +pull_requests: [19366] +--- + +Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately +these components have a number of shortcomings, such as the fact that they don't use the new +`bevy_picking` framework, or the fact that they are really only useful for creating buttons +and not other kinds of widgets like sliders. + +As an art form, games thrive on novelty: the typical game doesn't have boring, standardized controls +reminiscent of a productivity app, but instead will have beautiful, artistic widgets that are +in harmony with the game's overall visual theme. But writing new and unique widgets requires +skill and subtlety, particularly if we want first-class accessibility support. It's not a burden we +want to put on the average indie developer. + +In the web development world, "headless" widget libraries, such as +[headlessui](https://headlessui.com/) and [reakit](https://reakit.io/) have become popular. These +provide standardized widgets that implement all of the correct interactions and behavioral logic, +including integration with screen readers, but which are unstyled. It's the responsibility of the +game developer to provide the visual style and animation for the widgets, which can fit the overall +style of their game. + +With this release, Bevy introduces a collection of headless or "core" widgets. These are components +which can be added to any UI Node to get widget-like behavior. The core widget set includes buttons, +sliders, scrollbars, checkboxes, radio buttons, and more. This set will likely be expanded in +future releases. + +## Core Widgets + +The `bevy_core_widgets` crate provides implementations of unstyled widgets, such as buttons, +sliders, checkboxes and radio buttons. + +- `CoreButton` is a push button. It emits an activation event when clicked. +- (More to be added in subsequent PRs) + +## Widget Interaction States + +Many of the core widgets will define supplementary ECS components that are used to store the widget's +state, similar to how the old `Interaction` component worked, but in a way that is more flexible. +These components include: + +- `InteractionDisabled` - a boolean component used to indicate that a component should be + "grayed out" and non-interactive. Note that these disabled widgets are still visible and can + have keyboard focus (otherwise the user would have no way to discover them). +- `Hovered` is a simple boolean component that allows detection of whether the widget is being + hovered using regular Bevy change detection. +- `Checked` is a boolean component that stores the checked state of a checkbox or radio button. +- `Pressed` is used for a button-like widget, and will be true while the button is held down. + +The combination of `Hovered` and `Pressed` fulfills the same purpose as the old +`Interaction` component, except that now we can also represent "roll-off" behavior (the state where +you click on a button and then, while holding the mouse down, move the pointer out of the button's +bounds). It also provides additional flexibility in cases where a widget has multiple hoverable +parts, or cases where a widget is hoverable but doesn't have a pressed state (such as a tree-view +expansion toggle). + +## Widget Notifications + +Applications need a way to be notified when the user interacts with a widget. One way to do this +is using Bevy observers. This approach is useful in cases where you want the widget notifications +to bubble up the hierarchy. + +However, in UI work it's often desirable to connect widget interactions in ways that cut across the +hierarchy. For these kinds of situations, the core widgets offer an an alternate approach: one-shot +systems. You can register a function as a one-shot system and get the resulting `SystemId`. This can +then be passed as a parameter to the widget when it is constructed, so when the button subsequently +gets clicked or the slider is dragged, the system gets run. Because it's an ECS system, it can +inject any additional parameters it needs to update the Bevy world in response to the interaction. + +Most of the core widgets use "external state management" - something that is referred to in the +React.js world as "controlled" widgets. This means that for widgets that edit a parameter value +(such as checkboxes and sliders), the widget doesn't automatically update its own internal value, +but only sends a notification to the app telling it that the value needs to change. It's the +responsibility of the app to handle this notification and update the widget accordingly, and at the +same time update any other game state that is dependent on that parameter. + +There are multiple reasons for this, but the main one is this: typical game user interfaces aren't +just passive forms of fields to fill in, but more often represent a dynamic view of live data. As a +consequence, the displayed value of a widget may change even when the user is not directly +interacting with that widget. Externalizing the state avoids the need for two-way data binding, and +instead allows simpler one-way data binding that aligns well with the traditional "Model / View / +Controller" (MVC) design pattern. + +There are two exceptions to this rule about external state management. First, widgets which don't +edit a value, but which merely trigger an event (such as buttons), don't fall under this rule. +Second, widgets which have complex states that are too large and heavyweight to fit within a +notification event (such as a text editor) can choose to manage their state internally. These latter +widgets will need to implement a two-way data binding strategy.