Merge branch 'bevyengine:main' into proper-json-schema

This commit is contained in:
MevLyshkin 2025-06-11 13:14:39 +02:00 committed by GitHub
commit 648bd3d796
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
121 changed files with 2596 additions and 922 deletions

View File

@ -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: |

View File

@ -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

View File

@ -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},

View File

@ -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},
};

View File

@ -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 = [

View File

@ -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;

View File

@ -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

View File

@ -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<SystemId>,
}
fn button_on_key_event(
mut trigger: Trigger<FocusedInput<KeyboardInput>>,
q_state: Query<(&CoreButton, Has<InteractionDisabled>)>,
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<Pointer<Click>>,
mut q_state: Query<(&CoreButton, Has<Pressed>, Has<InteractionDisabled>)>,
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<Pointer<Press>>,
mut q_state: Query<(Entity, Has<InteractionDisabled>, Has<Pressed>), With<CoreButton>>,
focus: Option<ResMut<InputFocus>>,
focus_visible: Option<ResMut<InputFocusVisible>>,
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<Pointer<Release>>,
mut q_state: Query<(Entity, Has<InteractionDisabled>, Has<Pressed>), With<CoreButton>>,
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::<Pressed>();
}
}
}
fn button_on_pointer_drag_end(
mut trigger: Trigger<Pointer<DragEnd>>,
mut q_state: Query<(Entity, Has<InteractionDisabled>, Has<Pressed>), With<CoreButton>>,
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::<Pressed>();
}
}
}
fn button_on_pointer_cancel(
mut trigger: Trigger<Pointer<Cancel>>,
mut q_state: Query<(Entity, Has<InteractionDisabled>, Has<Pressed>), With<CoreButton>>,
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::<Pressed>();
}
}
}
/// 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);
}
}

View File

@ -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);
}
}

View File

@ -94,8 +94,8 @@ impl Plugin for DebugPickingPlugin {
log_event_debug::<pointer::PointerInput>.run_if(DebugPickingMode::is_noisy),
log_pointer_event_debug::<Over>,
log_pointer_event_debug::<Out>,
log_pointer_event_debug::<Pressed>,
log_pointer_event_debug::<Released>,
log_pointer_event_debug::<Press>,
log_pointer_event_debug::<Release>,
log_pointer_event_debug::<Click>,
log_pointer_event_trace::<Move>.run_if(DebugPickingMode::is_noisy),
log_pointer_event_debug::<DragStart>,

View File

@ -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) {}

View File

@ -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<TokenStream2> {
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)
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -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<String>
/// }
/// ```
///
/// 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<ChildOf, Spawn<Marker>>,
/// }
/// ```
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<T: Eq + Ord + Copy>(source: &mut Vec<T>, 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<ChildOf, Spawn<C>>,
}
#[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::<Children>().is_some());
}
#[test]
fn component_hook_order_spawn_despawn() {
let mut world = World::new();

View File

@ -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<Entity>);
///
/// let mut world = World::new();
/// world.init_resource::<TrackedEntities>();
///
/// // 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::<MyTrackedComponent>().on_add(|mut world, context| {
/// let mut tracked_entities = world.resource_mut::<TrackedEntities>();
/// tracked_entities.0.insert(context.entity);
/// });
///
/// world.register_component_hooks::<MyTrackedComponent>().on_remove(|mut world, context| {
/// let mut tracked_entities = world.resource_mut::<TrackedEntities>();
/// tracked_entities.0.remove(&context.entity);
/// });
///
/// let entity = world.spawn(MyTrackedComponent).id();
/// let tracked_entities = world.resource::<TrackedEntities>();
/// assert!(tracked_entities.0.contains(&entity));
///
/// world.despawn(entity);
/// let tracked_entities = world.resource::<TrackedEntities>();
/// assert!(!tracked_entities.0.contains(&entity));
/// ```
#[derive(Debug, Clone, Default)]
pub struct ComponentHooks {
pub(crate) on_add: Option<ComponentHook>,
pub(crate) on_insert: Option<ComponentHook>,
pub(crate) on_replace: Option<ComponentHook>,
pub(crate) on_remove: Option<ComponentHook>,
pub(crate) on_despawn: Option<ComponentHook>,
}
impl ComponentHooks {
pub(crate) fn update_from_component<C: Component + ?Sized>(&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<CheckChangeTicks>, mut schedule: ResMut<CustomSchedule>| {
/// 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> {

View File

@ -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::<EventWrapperComponent<Self>>()
@ -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<ComponentId> {
world.component_id::<EventWrapperComponent<Self>>()

View File

@ -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},

View File

@ -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,
},
};

View File

@ -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<Entity>);
///
/// let mut world = World::new();
/// world.init_resource::<TrackedEntities>();
///
/// // 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::<MyTrackedComponent>().on_add(|mut world, context| {
/// let mut tracked_entities = world.resource_mut::<TrackedEntities>();
/// tracked_entities.0.insert(context.entity);
/// });
///
/// world.register_component_hooks::<MyTrackedComponent>().on_remove(|mut world, context| {
/// let mut tracked_entities = world.resource_mut::<TrackedEntities>();
/// tracked_entities.0.remove(&context.entity);
/// });
///
/// let entity = world.spawn(MyTrackedComponent).id();
/// let tracked_entities = world.resource::<TrackedEntities>();
/// assert!(tracked_entities.0.contains(&entity));
///
/// world.despawn(entity);
/// let tracked_entities = world.resource::<TrackedEntities>();
/// assert!(!tracked_entities.0.contains(&entity));
/// ```
#[derive(Debug, Clone, Default)]
pub struct ComponentHooks {
pub(crate) on_add: Option<ComponentHook>,
pub(crate) on_insert: Option<ComponentHook>,
pub(crate) on_replace: Option<ComponentHook>,
pub(crate) on_remove: Option<ComponentHook>,
pub(crate) on_despawn: Option<ComponentHook>,
}
impl ComponentHooks {
pub(crate) fn update_from_component<C: Component + ?Sized>(&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<RemovedComponentEntity>`.
#[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<RemovedComponentEntity>`] so that we
/// can differentiate events between components.
#[derive(Debug)]
pub struct RemovedComponentReader<T>
where
T: Component,
{
reader: EventCursor<RemovedComponentEntity>,
marker: PhantomData<T>,
}
impl<T: Component> Default for RemovedComponentReader<T> {
fn default() -> Self {
Self {
reader: Default::default(),
marker: PhantomData,
}
}
}
impl<T: Component> Deref for RemovedComponentReader<T> {
type Target = EventCursor<RemovedComponentEntity>;
fn deref(&self) -> &Self::Target {
&self.reader
}
}
impl<T: Component> DerefMut for RemovedComponentReader<T> {
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<ComponentId, Events<RemovedComponentEntity>>,
}
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<Item = (&ComponentId, &Events<RemovedComponentEntity>)> {
self.event_sets.iter()
}
/// Gets the event storage for a given component.
pub fn get(
&self,
component_id: impl Into<ComponentId>,
) -> Option<&Events<RemovedComponentEntity>> {
self.event_sets.get(component_id.into())
}
/// Sends a removal event for the specified component.
pub fn send(&mut self, component_id: impl Into<ComponentId>, 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<MyComponent>) {
/// 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<T>>,
event_sets: &'w RemovedComponentEvents,
}
/// Iterator over entities that had a specific component removed.
///
/// See [`RemovedComponents`].
pub type RemovedIter<'a> = iter::Map<
iter::Flatten<option::IntoIter<iter::Cloned<EventIterator<'a, RemovedComponentEntity>>>>,
fn(RemovedComponentEntity) -> Entity,
>;
/// Iterator over entities that had a specific component removed.
///
/// See [`RemovedComponents`].
pub type RemovedIterWithId<'a> = iter::Map<
iter::Flatten<option::IntoIter<EventIteratorWithId<'a, RemovedComponentEntity>>>,
fn(
(&RemovedComponentEntity, EventId<RemovedComponentEntity>),
) -> (Entity, EventId<RemovedComponentEntity>),
>;
fn map_id_events(
(entity, id): (&RemovedComponentEntity, EventId<RemovedComponentEntity>),
) -> (Entity, EventId<RemovedComponentEntity>) {
(entity.clone().into(), id)
}
// For all practical purposes, the api surface of `RemovedComponents<T>`
// should be similar to `EventReader<T>` to reduce confusion.
impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> {
/// Fetch underlying [`EventCursor`].
pub fn reader(&self) -> &EventCursor<RemovedComponentEntity> {
&self.reader
}
/// Fetch underlying [`EventCursor`] mutably.
pub fn reader_mut(&mut self) -> &mut EventCursor<RemovedComponentEntity> {
&mut self.reader
}
/// Fetch underlying [`Events`].
pub fn events(&self) -> Option<&Events<RemovedComponentEntity>> {
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<T>,
&Events<RemovedComponentEntity>,
)> {
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()
}
}

View File

@ -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;

View File

@ -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<ArchetypeFlags> {
use crate::lifecycle::*;
match event_type {
ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER),
ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER),

View File

@ -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<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
}
}
/// 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.

View File

@ -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;

View File

@ -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<RemovedComponentEntity>`.
#[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<RemovedComponentEntity>`] so that we
/// can differentiate events between components.
#[derive(Debug)]
pub struct RemovedComponentReader<T>
where
T: Component,
{
reader: EventCursor<RemovedComponentEntity>,
marker: PhantomData<T>,
}
impl<T: Component> Default for RemovedComponentReader<T> {
fn default() -> Self {
Self {
reader: Default::default(),
marker: PhantomData,
}
}
}
impl<T: Component> Deref for RemovedComponentReader<T> {
type Target = EventCursor<RemovedComponentEntity>;
fn deref(&self) -> &Self::Target {
&self.reader
}
}
impl<T: Component> DerefMut for RemovedComponentReader<T> {
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<ComponentId, Events<RemovedComponentEntity>>,
}
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<Item = (&ComponentId, &Events<RemovedComponentEntity>)> {
self.event_sets.iter()
}
/// Gets the event storage for a given component.
pub fn get(
&self,
component_id: impl Into<ComponentId>,
) -> Option<&Events<RemovedComponentEntity>> {
self.event_sets.get(component_id.into())
}
/// Sends a removal event for the specified component.
pub fn send(&mut self, component_id: impl Into<ComponentId>, 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<T>>`
/// 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<MyComponent>) {
/// 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<T>>,
event_sets: &'w RemovedComponentEvents,
}
/// Iterator over entities that had a specific component removed.
///
/// See [`RemovedComponents`].
pub type RemovedIter<'a> = iter::Map<
iter::Flatten<option::IntoIter<iter::Cloned<EventIterator<'a, RemovedComponentEntity>>>>,
fn(RemovedComponentEntity) -> Entity,
>;
/// Iterator over entities that had a specific component removed.
///
/// See [`RemovedComponents`].
pub type RemovedIterWithId<'a> = iter::Map<
iter::Flatten<option::IntoIter<EventIteratorWithId<'a, RemovedComponentEntity>>>,
fn(
(&RemovedComponentEntity, EventId<RemovedComponentEntity>),
) -> (Entity, EventId<RemovedComponentEntity>),
>;
fn map_id_events(
(entity, id): (&RemovedComponentEntity, EventId<RemovedComponentEntity>),
) -> (Entity, EventId<RemovedComponentEntity>) {
(entity.clone().into(), id)
}
// For all practical purposes, the api surface of `RemovedComponents<T>`
// should be similar to `EventReader<T>` to reduce confusion.
impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> {
/// Fetch underlying [`EventCursor`].
pub fn reader(&self) -> &EventCursor<RemovedComponentEntity> {
&self.reader
}
/// Fetch underlying [`EventCursor`] mutably.
pub fn reader_mut(&mut self) -> &mut EventCursor<RemovedComponentEntity> {
&mut self.reader
}
/// Fetch underlying [`Events`].
pub fn events(&self) -> Option<&Events<RemovedComponentEntity>> {
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<T>,
&Events<RemovedComponentEntity>,
)> {
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()
}
}

View File

@ -11,8 +11,18 @@ pub type BoxedCondition<In = ()> = Box<dyn ReadOnlySystem<In = In, Out = bool>>;
/// A system that determines if one or more scheduled systems should run.
///
/// Implemented for functions and closures that convert into [`System<Out=bool>`](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<Out = bool>`](System), [`System<Out = Result<(), BevyError>>`](System) or
/// [`System<Out = Result<bool, BevyError>>`](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<bool, BevyError>`: the condition returns `true` if and only if the implementing system returns `Ok(true)`.
///
/// # Marker type parameter
///
@ -31,7 +41,7 @@ pub type BoxedCondition<In = ()> = Box<dyn ReadOnlySystem<In = In, Out = bool>>;
/// ```
///
/// # 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<In = ()> = Box<dyn ReadOnlySystem<In = In, Out = bool>>;
/// # assert!(!world.resource::<DidRun>().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<In = ()> = Box<dyn ReadOnlySystem<In = In, Out = bool>>;
/// # world.insert_resource(DidRun(false));
/// # app.run(&mut world);
/// # assert!(world.resource::<DidRun>().0);
pub trait SystemCondition<Marker, In: SystemInput = ()>:
sealed::SystemCondition<Marker, In>
/// ```
///
/// A condition returning a `Result<(), BevyError>`
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # #[derive(Component)] struct Player;
/// fn player_exists(q_player: Query<(), With<Player>>) -> Result {
/// Ok(q_player.single()?)
/// }
///
/// # let mut app = Schedule::default();
/// # #[derive(Resource)] struct DidRun(bool);
/// # fn my_system(mut did_run: ResMut<DidRun>) { 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::<DidRun>().0);
/// # world.spawn(Player);
/// # app.run(&mut world);
/// # assert!(world.resource::<DidRun>().0);
pub trait SystemCondition<Marker, In: SystemInput = (), Out = bool>:
sealed::SystemCondition<Marker, In, Out>
{
/// 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<Marker, In: SystemInput = ()>:
}
}
impl<Marker, In: SystemInput, F> SystemCondition<Marker, In> for F where
F: sealed::SystemCondition<Marker, In>
impl<Marker, In: SystemInput, Out, F> SystemCondition<Marker, In, Out> for F where
F: sealed::SystemCondition<Marker, In, Out>
{
}
mod sealed {
use crate::system::{IntoSystem, ReadOnlySystem, SystemInput};
use crate::{
error::BevyError,
system::{IntoSystem, ReadOnlySystem, SystemInput},
};
pub trait SystemCondition<Marker, In: SystemInput>:
IntoSystem<In, bool, Marker, System = Self::ReadOnlySystem>
pub trait SystemCondition<Marker, In: SystemInput, Out>:
IntoSystem<In, Out, Marker, System = Self::ReadOnlySystem>
{
// This associated type is necessary to let the compiler
// know that `Self::System` is `ReadOnlySystem`.
type ReadOnlySystem: ReadOnlySystem<In = In, Out = bool>;
type ReadOnlySystem: ReadOnlySystem<In = In, Out = Out>;
fn into_condition_system(self) -> impl ReadOnlySystem<In = In, Out = bool>;
}
impl<Marker, In: SystemInput, F> SystemCondition<Marker, In> for F
impl<Marker, In: SystemInput, F> SystemCondition<Marker, In, bool> for F
where
F: IntoSystem<In, bool, Marker>,
F::System: ReadOnlySystem,
{
type ReadOnlySystem = F::System;
fn into_condition_system(self) -> impl ReadOnlySystem<In = In, Out = bool> {
IntoSystem::into_system(self)
}
}
impl<Marker, In: SystemInput, F> SystemCondition<Marker, In, Result<(), BevyError>> for F
where
F: IntoSystem<In, Result<(), BevyError>, Marker>,
F::System: ReadOnlySystem,
{
type ReadOnlySystem = F::System;
fn into_condition_system(self) -> impl ReadOnlySystem<In = In, Out = bool> {
IntoSystem::into_system(self.map(|result| result.is_ok()))
}
}
impl<Marker, In: SystemInput, F> SystemCondition<Marker, In, Result<bool, BevyError>> for F
where
F: IntoSystem<In, Result<bool, BevyError>, Marker>,
F::System: ReadOnlySystem,
{
type ReadOnlySystem = F::System;
fn into_condition_system(self) -> impl ReadOnlySystem<In = In, Out = bool> {
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},
};

View File

@ -14,8 +14,8 @@ use crate::{
system::{BoxedSystem, InfallibleSystemWrapper, IntoSystem, ScheduleSystem, System},
};
fn new_condition<M>(condition: impl SystemCondition<M>) -> BoxedCondition {
let condition_system = IntoSystem::into_system(condition);
fn new_condition<M, Out>(condition: impl SystemCondition<M, (), Out>) -> 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<T: Schedulable<Metadata = GraphInfo, GroupMetadata
///
/// Use [`distributive_run_if`](IntoScheduleConfigs::distributive_run_if) if you want the
/// condition to be evaluated for each individual system, right before one is run.
fn run_if<M>(self, condition: impl SystemCondition<M>) -> ScheduleConfigs<T> {
fn run_if<M, Out>(self, condition: impl SystemCondition<M, (), Out>) -> ScheduleConfigs<T> {
self.into_configs().run_if(condition)
}
@ -535,7 +535,7 @@ impl<T: Schedulable<Metadata = GraphInfo, GroupMetadata = Chain>> IntoScheduleCo
self
}
fn run_if<M>(mut self, condition: impl SystemCondition<M>) -> ScheduleConfigs<T> {
fn run_if<M, Out>(mut self, condition: impl SystemCondition<M, (), Out>) -> ScheduleConfigs<T> {
self.run_if_dyn(new_condition(condition));
self
}

View File

@ -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<ComponentId> {
// This system accesses no components.
const { &Access::new() }
}
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId> {
// This system accesses no components.
const { &FilteredAccessSet::new() }
}

View File

@ -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<u32>);
#[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::<SystemOrder>().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::<SystemOrder>().0, vec![0]);
}
#[test]
fn system_with_condition_result_unit() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.init_resource::<SystemOrder>();
schedule.add_systems(
make_function_system(0).run_if(|| Err::<(), BevyError>(core::fmt::Error.into())),
);
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![]);
schedule.add_systems(make_function_system(1).run_if(|| Ok(())));
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![1]);
}
#[test]
fn system_with_condition_result_bool() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.init_resource::<SystemOrder>();
schedule.add_systems((
make_function_system(0).run_if(|| Err::<bool, BevyError>(core::fmt::Error.into())),
make_function_system(1).run_if(|| Ok(false)),
));
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![]);
schedule.add_systems(make_function_system(2).run_if(|| Ok(true)));
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![2]);
}
#[test]
fn systems_with_distributive_condition() {
let mut world = World::default();

View File

@ -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)),

View File

@ -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)]`"
)]

View File

@ -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"
);
}
}

View File

@ -127,10 +127,6 @@ where
self.name.clone()
}
fn component_access(&self) -> &crate::query::Access<crate::component::ComponentId> {
self.system.component_access()
}
fn component_access_set(
&self,
) -> &crate::query::FilteredAccessSet<crate::component::ComponentId> {

View File

@ -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<ComponentId> {
self.component_access_set.combined_access()
}
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId> {
&self.component_access_set
}
@ -363,10 +359,6 @@ where
self.name.clone()
}
fn component_access(&self) -> &Access<ComponentId> {
self.component_access_set.combined_access()
}
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId> {
&self.component_access_set
}

View File

@ -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<ComponentId> {
self.system_meta.component_access_set.combined_access()
}
#[inline]
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId> {
&self.system_meta.component_access_set

View File

@ -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<ComponentId> {
self.system_meta.component_access_set.combined_access()
}
#[inline]
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId> {
&self.system_meta.component_access_set

View File

@ -97,7 +97,7 @@
//! - [`EventWriter`](crate::event::EventWriter)
//! - [`NonSend`] and `Option<NonSend>`
//! - [`NonSendMut`] and `Option<NonSendMut>`
//! - [`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::<B>())

View File

@ -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<ComponentId> {
self.observer.component_access()
}
#[inline]
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId> {
self.observer.component_access_set()

View File

@ -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<S: System<In = ()>> System for InfallibleSystemWrapper<S> {
self.0.type_id()
}
#[inline]
fn component_access(&self) -> &Access<ComponentId> {
self.0.component_access()
}
#[inline]
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId> {
self.0.component_access_set()
@ -154,10 +149,6 @@ where
self.system.name()
}
fn component_access(&self) -> &Access<ComponentId> {
self.system.component_access()
}
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId> {
self.system.component_access_set()
}
@ -256,10 +247,6 @@ where
self.system.name()
}
fn component_access(&self) -> &Access<ComponentId> {
self.system.component_access()
}
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId> {
self.system.component_access_set()
}

View File

@ -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::<Self>()
}
/// Returns the system's component [`Access`].
fn component_access(&self) -> &Access<ComponentId>;
/// Returns the system's component [`FilteredAccessSet`].
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId>;

View File

@ -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;

View File

@ -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.

View File

@ -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::<T>() {
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<Mutability = Mutable>> Entry<'w, 'a, T> {
impl<'w, 'a, T: Component<Mutability = Mutable>> ComponentEntry<'w, 'a, T> {
/// Provides in-place mutable access to an occupied entry.
///
/// # Examples
@ -2885,17 +2883,17 @@ impl<'w, 'a, T: Component<Mutability = Mutable>> Entry<'w, 'a, T> {
#[inline]
pub fn and_modify<F: FnOnce(Mut<'_, T>)>(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<F: FnOnce() -> T>(self, default: F) -> OccupiedEntry<'w, 'a, T> {
pub fn or_insert_with<F: FnOnce() -> 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<T>,
}
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::<Comp>() {
/// if let ComponentEntry::Occupied(o) = entity.entry::<Comp>() {
/// 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::<T>().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::<Comp>() {
/// if let ComponentEntry::Occupied(mut o) = entity.entry::<Comp>() {
/// 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::<Comp>() {
/// if let ComponentEntry::Occupied(o) = entity.entry::<Comp>() {
/// 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<Mutability = Mutable>> OccupiedEntry<'w, 'a, T> {
impl<'w, 'a, T: Component<Mutability = Mutable>> 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::<Comp>() {
/// if let ComponentEntry::Occupied(mut o) = entity.entry::<Comp>() {
/// o.get_mut().0 += 10;
/// assert_eq!(o.get().0, 15);
///
@ -3113,28 +3111,28 @@ impl<'w, 'a, T: Component<Mutability = Mutable>> 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::<T>().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::<Comp>() {
/// if let ComponentEntry::Occupied(o) = entity.entry::<Comp>() {
/// o.into_mut().0 += 10;
/// }
///
@ -3142,40 +3140,40 @@ impl<'w, 'a, T: Component<Mutability = Mutable>> 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<T>,
}
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::<Comp>() {
/// if let ComponentEntry::Vacant(v) = entity.entry::<Comp>() {
/// 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,

View File

@ -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;
}

View File

@ -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.
///

View File

@ -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;

View File

@ -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},

View File

@ -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" }

View File

@ -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;

View File

@ -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

View File

@ -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},

View File

@ -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<Cancel>>,
click_events: EventWriter<'w, Pointer<Click>>,
pressed_events: EventWriter<'w, Pointer<Pressed>>,
pressed_events: EventWriter<'w, Pointer<Press>>,
drag_drop_events: EventWriter<'w, Pointer<DragDrop>>,
drag_end_events: EventWriter<'w, Pointer<DragEnd>>,
drag_enter_events: EventWriter<'w, Pointer<DragEnter>>,
@ -412,7 +412,7 @@ pub struct PickingEventWriters<'w> {
move_events: EventWriter<'w, Pointer<Move>>,
out_events: EventWriter<'w, Pointer<Out>>,
over_events: EventWriter<'w, Pointer<Over>>,
released_events: EventWriter<'w, Pointer<Released>>,
released_events: EventWriter<'w, Pointer<Release>>,
}
/// 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(),
},

View File

@ -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<Res<HoverMap>>,
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<Res<HoverMap>>,
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::<Hovered>().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::<Hovered>().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::<Hovered>().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::<DirectlyHovered>()
.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::<DirectlyHovered>()
.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::<DirectlyHovered>()
.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::<DirectlyHovered>()
.unwrap();
assert!(!hover.get());
assert!(hover.is_changed());
}
}

View File

@ -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::<Self>()
.register_type::<Pickable>()
.register_type::<hover::PickingInteraction>()
.register_type::<hover::Hovered>()
.register_type::<pointer::PointerId>()
.register_type::<pointer::PointerLocation>()
.register_type::<pointer::PointerPress>()
@ -414,7 +416,7 @@ impl Plugin for InteractionPlugin {
.init_resource::<PointerState>()
.add_event::<Pointer<Cancel>>()
.add_event::<Pointer<Click>>()
.add_event::<Pointer<Pressed>>()
.add_event::<Pointer<Press>>()
.add_event::<Pointer<DragDrop>>()
.add_event::<Pointer<DragEnd>>()
.add_event::<Pointer<DragEnter>>()
@ -425,11 +427,16 @@ impl Plugin for InteractionPlugin {
.add_event::<Pointer<Move>>()
.add_event::<Pointer<Out>>()
.add_event::<Pointer<Over>>()
.add_event::<Pointer<Released>>()
.add_event::<Pointer<Release>>()
.add_event::<Pointer<Scroll>>()
.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),
);

View File

@ -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},
};

View File

@ -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<BlendState>,
/// The clear color operation to perform on the final render target texture.
clear_color: ClearColorConfig,

View File

@ -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);

View File

@ -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,
});

View File

@ -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::<RenderEntity>() {
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::{

View File

@ -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::*;

View File

@ -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},

View File

@ -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.

View File

@ -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

View File

@ -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,
};
}

View File

@ -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<Font>,
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
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);

View File

@ -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<String> 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<JustifyText> for cosmic_text::Align {
fn from(justify: JustifyText) -> Self {
impl From<Justify> 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),

View File

@ -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<Font> = 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

View File

@ -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<OnAdd, InteractionDisabled>,
mut world: DeferredWorld,
) {
let mut entity = world.entity_mut(trigger.target().unwrap());
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
accessibility.set_disabled();
}
}
pub(crate) fn on_remove_disabled(
trigger: Trigger<OnRemove, InteractionDisabled>,
mut world: DeferredWorld,
) {
let mut entity = world.entity_mut(trigger.target().unwrap());
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
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<OnInsert, Checked>, mut world: DeferredWorld) {
let mut entity = world.entity_mut(trigger.target().unwrap());
let checked = entity.get::<Checked>().unwrap().get();
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
accessibility.set_toggled(match checked {
true => accesskit::Toggled::True,
false => accesskit::Toggled::False,
});
}
}
pub(crate) fn on_remove_is_checked(trigger: Trigger<OnRemove, Checked>, mut world: DeferredWorld) {
let mut entity = world.entity_mut(trigger.target().unwrap());
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
accessibility.set_toggled(accesskit::Toggled::False);
}
}

View File

@ -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,
};

View File

@ -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 <https://cssreference.io/flexbox/>)
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),

View File

@ -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<Color>) -> &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

View File

@ -138,8 +138,8 @@ impl From<Handle<Image>> 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]

View File

@ -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<Font> = 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

View File

@ -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};

View File

@ -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};

View File

@ -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|

View File

@ -129,7 +129,7 @@ fn setup_sprites(mut commands: Commands, asset_server: Res<AssetServer>) {
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,

View File

@ -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,
)],

View File

@ -40,7 +40,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
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<AssetServer>) {
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<AssetServer>) {
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<AssetServer>) {
// 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)),
));

View File

@ -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()

View File

@ -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()
},
));

View File

@ -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),

View File

@ -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

View File

@ -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 {

View File

@ -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();

View File

@ -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)),

View File

@ -54,7 +54,7 @@ fn spawn_text(mut commands: Commands, mut reader: EventReader<StreamEvent>) {
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),
));
}

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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 {

View File

@ -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())),

View File

@ -158,7 +158,7 @@ fn setup_scene(
..default()
},
TextColor::BLACK,
TextLayout::new_with_justify(JustifyText::Center),
TextLayout::new_with_justify(Justify::Center),
));
}

View File

@ -91,8 +91,8 @@ fn setup_scene(
))
.observe(update_material_on::<Pointer<Over>>(hover_matl.clone()))
.observe(update_material_on::<Pointer<Out>>(white_matl.clone()))
.observe(update_material_on::<Pointer<Pressed>>(pressed_matl.clone()))
.observe(update_material_on::<Pointer<Released>>(hover_matl.clone()))
.observe(update_material_on::<Pointer<Press>>(pressed_matl.clone()))
.observe(update_material_on::<Pointer<Release>>(hover_matl.clone()))
.observe(rotate_on_drag);
}
@ -114,8 +114,8 @@ fn setup_scene(
))
.observe(update_material_on::<Pointer<Over>>(hover_matl.clone()))
.observe(update_material_on::<Pointer<Out>>(white_matl.clone()))
.observe(update_material_on::<Pointer<Pressed>>(pressed_matl.clone()))
.observe(update_material_on::<Pointer<Released>>(hover_matl.clone()))
.observe(update_material_on::<Pointer<Press>>(pressed_matl.clone()))
.observe(update_material_on::<Pointer<Release>>(hover_matl.clone()))
.observe(rotate_on_drag);
}

View File

@ -63,8 +63,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
))
.observe(recolor_on::<Pointer<Over>>(Color::srgb(0.0, 1.0, 1.0)))
.observe(recolor_on::<Pointer<Out>>(Color::BLACK))
.observe(recolor_on::<Pointer<Pressed>>(Color::srgb(1.0, 1.0, 0.0)))
.observe(recolor_on::<Pointer<Released>>(Color::srgb(0.0, 1.0, 1.0)));
.observe(recolor_on::<Pointer<Press>>(Color::srgb(1.0, 1.0, 0.0)))
.observe(recolor_on::<Pointer<Release>>(Color::srgb(0.0, 1.0, 1.0)));
commands
.spawn((
@ -83,8 +83,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
))
.observe(recolor_on::<Pointer<Over>>(Color::srgb(0.0, 1.0, 0.0)))
.observe(recolor_on::<Pointer<Out>>(Color::srgb(1.0, 0.0, 0.0)))
.observe(recolor_on::<Pointer<Pressed>>(Color::srgb(0.0, 0.0, 1.0)))
.observe(recolor_on::<Pointer<Released>>(Color::srgb(0.0, 1.0, 0.0)));
.observe(recolor_on::<Pointer<Press>>(Color::srgb(0.0, 0.0, 1.0)))
.observe(recolor_on::<Pointer<Release>>(Color::srgb(0.0, 1.0, 0.0)));
}
});
}
@ -145,8 +145,8 @@ fn setup_atlas(
))
.observe(recolor_on::<Pointer<Over>>(Color::srgb(0.0, 1.0, 1.0)))
.observe(recolor_on::<Pointer<Out>>(Color::srgb(1.0, 1.0, 1.0)))
.observe(recolor_on::<Pointer<Pressed>>(Color::srgb(1.0, 1.0, 0.0)))
.observe(recolor_on::<Pointer<Released>>(Color::srgb(0.0, 1.0, 1.0)));
.observe(recolor_on::<Pointer<Press>>(Color::srgb(1.0, 1.0, 0.0)))
.observe(recolor_on::<Pointer<Release>>(Color::srgb(0.0, 1.0, 1.0)));
}
// An observer listener that changes the target entity's color.

View File

@ -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();

View File

@ -74,7 +74,7 @@ fn setup(mut commands: Commands, args: Res<Args>) {
..Default::default()
};
let text_block = TextLayout {
justify: JustifyText::Left,
justify: Justify::Left,
linebreak: LineBreak::AnyCharacter,
};

View File

@ -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<FontHandle>, args: Res<Args>) {
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,

View File

@ -69,7 +69,7 @@ fn spawn(mut commands: Commands, asset_server: Res<AssetServer>) {
.spawn((
Text2d::default(),
TextLayout {
justify: JustifyText::Center,
justify: Justify::Center,
linebreak: LineBreak::AnyCharacter,
},
TextBounds::default(),

View File

@ -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<TextBounds>,
) {
commands.spawn((

Some files were not shown because too many files have changed in this diff Show More