diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 37db848558..526ac4c401 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -95,7 +95,7 @@ jobs:
- name: CI job
# To run the tests one item at a time for troubleshooting, use
# cargo --quiet test --lib -- --list | sed 's/: test$//' | MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-disable-weak-memory-emulation" xargs -n1 cargo miri test -p bevy_ecs --lib -- --exact
- run: cargo miri test -p bevy_ecs
+ run: cargo miri test -p bevy_ecs --features bevy_utils/debug
env:
# -Zrandomize-layout makes sure we dont rely on the layout of anything that might change
RUSTFLAGS: -Zrandomize-layout
@@ -247,7 +247,7 @@ jobs:
- name: Check wasm
run: cargo check --target wasm32-unknown-unknown -Z build-std=std,panic_abort
env:
- RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory -D warnings"
+ RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory"
markdownlint:
runs-on: ubuntu-latest
@@ -293,7 +293,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Check for typos
- uses: crate-ci/typos@v1.32.0
+ uses: crate-ci/typos@v1.33.1
- name: Typos info
if: failure()
run: |
diff --git a/Cargo.toml b/Cargo.toml
index 2d9524a527..363e85b3b8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -74,7 +74,6 @@ allow_attributes_without_reason = "warn"
[workspace.lints.rust]
missing_docs = "warn"
-mismatched_lifetime_syntaxes = "allow"
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] }
unsafe_code = "deny"
unsafe_op_in_unsafe_fn = "warn"
@@ -136,6 +135,7 @@ default = [
"bevy_audio",
"bevy_color",
"bevy_core_pipeline",
+ "bevy_core_widgets",
"bevy_anti_aliasing",
"bevy_gilrs",
"bevy_gizmos",
@@ -167,6 +167,7 @@ default = [
"vorbis",
"webgl2",
"x11",
+ "debug",
]
# Recommended defaults for no_std applications
@@ -249,6 +250,15 @@ bevy_render = ["bevy_internal/bevy_render", "bevy_color"]
# Provides scene functionality
bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"]
+# Provides raytraced lighting (experimental)
+bevy_solari = [
+ "bevy_internal/bevy_solari",
+ "bevy_asset",
+ "bevy_core_pipeline",
+ "bevy_pbr",
+ "bevy_render",
+]
+
# Provides sprite functionality
bevy_sprite = [
"bevy_internal/bevy_sprite",
@@ -295,6 +305,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"]
@@ -497,7 +510,10 @@ file_watcher = ["bevy_internal/file_watcher"]
embedded_watcher = ["bevy_internal/embedded_watcher"]
# Enable stepping-based debugging of Bevy systems
-bevy_debug_stepping = ["bevy_internal/bevy_debug_stepping"]
+bevy_debug_stepping = [
+ "bevy_internal/bevy_debug_stepping",
+ "bevy_internal/debug",
+]
# Enables the meshlet renderer for dense high-poly scenes (experimental)
meshlet = ["bevy_internal/meshlet"]
@@ -547,6 +563,9 @@ web = ["bevy_internal/web"]
# Enable hotpatching of Bevy systems
hotpatching = ["bevy_internal/hotpatching"]
+# Enable collecting debug information about systems and components to help with diagnostics
+debug = ["bevy_internal/debug"]
+
[dependencies]
bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false }
tracing = { version = "0.1", default-features = false, optional = true }
@@ -558,7 +577,7 @@ bevy_dylib = { path = "crates/bevy_dylib", version = "0.16.0-dev", default-featu
[dev-dependencies]
rand = "0.8.0"
rand_chacha = "0.3.1"
-ron = "0.8.0"
+ron = "0.10"
flate2 = "1.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.140"
@@ -597,7 +616,7 @@ web-sys = { version = "0.3", features = ["Window"] }
[[example]]
name = "context_menu"
-path = "examples/usages/context_menu.rs"
+path = "examples/usage/context_menu.rs"
doc-scrape-examples = true
[package.metadata.example.context_menu]
@@ -1266,6 +1285,18 @@ description = "Load a cubemap texture onto a cube like a skybox and cycle throug
category = "3D Rendering"
wasm = false
+[[example]]
+name = "solari"
+path = "examples/3d/solari.rs"
+doc-scrape-examples = true
+required-features = ["bevy_solari"]
+
+[package.metadata.example.solari]
+name = "Solari"
+description = "Demonstrates realtime dynamic global illumination rendering using Bevy Solari."
+category = "3D Rendering"
+wasm = false
+
[[example]]
name = "spherical_area_lights"
path = "examples/3d/spherical_area_lights.rs"
@@ -2082,6 +2113,7 @@ wasm = false
name = "dynamic"
path = "examples/ecs/dynamic.rs"
doc-scrape-examples = true
+required-features = ["debug"]
[package.metadata.example.dynamic]
name = "Dynamic ECS"
@@ -3563,6 +3595,17 @@ description = "Illustrates how to use 9 Slicing for TextureAtlases in UI"
category = "UI (User Interface)"
wasm = true
+[[example]]
+name = "ui_transform"
+path = "examples/ui/ui_transform.rs"
+doc-scrape-examples = true
+
+[package.metadata.example.ui_transform]
+name = "UI Transform"
+description = "An example demonstrating how to translate, rotate and scale UI elements."
+category = "UI (User Interface)"
+wasm = true
+
[[example]]
name = "viewport_debug"
path = "examples/ui/viewport_debug.rs"
@@ -4447,3 +4490,25 @@ name = "Hotpatching Systems"
description = "Demonstrates how to hotpatch systems"
category = "ECS (Entity Component System)"
wasm = false
+
+[[example]]
+name = "core_widgets"
+path = "examples/ui/core_widgets.rs"
+doc-scrape-examples = true
+
+[package.metadata.example.core_widgets]
+name = "Core Widgets"
+description = "Demonstrates use of core (headless) widgets in Bevy UI"
+category = "UI (User Interface)"
+wasm = true
+
+[[example]]
+name = "core_widgets_observers"
+path = "examples/ui/core_widgets_observers.rs"
+doc-scrape-examples = true
+
+[package.metadata.example.core_widgets_observers]
+name = "Core Widgets (w/Observers)"
+description = "Demonstrates use of core (headless) widgets in Bevy UI, with Observers"
+category = "UI (User Interface)"
+wasm = true
diff --git a/assets/branding/bevy_solari.svg b/assets/branding/bevy_solari.svg
new file mode 100644
index 0000000000..65b996493f
--- /dev/null
+++ b/assets/branding/bevy_solari.svg
@@ -0,0 +1,113 @@
+
+
diff --git a/benches/benches/bevy_ecs/events/iter.rs b/benches/benches/bevy_ecs/events/iter.rs
index dc20bc3395..9ad17ed8c8 100644
--- a/benches/benches/bevy_ecs/events/iter.rs
+++ b/benches/benches/bevy_ecs/events/iter.rs
@@ -1,6 +1,6 @@
use bevy_ecs::prelude::*;
-#[derive(Event)]
+#[derive(Event, BufferedEvent)]
struct BenchEvent([u8; SIZE]);
pub struct Benchmark(Events>);
diff --git a/benches/benches/bevy_ecs/events/send.rs b/benches/benches/bevy_ecs/events/send.rs
index fa996b50aa..be8934e789 100644
--- a/benches/benches/bevy_ecs/events/send.rs
+++ b/benches/benches/bevy_ecs/events/send.rs
@@ -1,6 +1,6 @@
use bevy_ecs::prelude::*;
-#[derive(Event)]
+#[derive(Event, BufferedEvent)]
struct BenchEvent([u8; SIZE]);
impl Default for BenchEvent {
diff --git a/benches/benches/bevy_ecs/observers/propagation.rs b/benches/benches/bevy_ecs/observers/propagation.rs
index 65c15f7308..808c3727d5 100644
--- a/benches/benches/bevy_ecs/observers/propagation.rs
+++ b/benches/benches/bevy_ecs/observers/propagation.rs
@@ -61,14 +61,10 @@ pub fn event_propagation(criterion: &mut Criterion) {
group.finish();
}
-#[derive(Clone, Component)]
+#[derive(Event, EntityEvent, Clone, Component)]
+#[entity_event(traversal = &'static ChildOf, auto_propagate)]
struct TestEvent {}
-impl Event for TestEvent {
- type Traversal = &'static ChildOf;
- const AUTO_PROPAGATE: bool = true;
-}
-
fn send_events(world: &mut World, leaves: &[Entity]) {
let target = leaves.iter().choose(&mut rand::thread_rng()).unwrap();
@@ -117,6 +113,6 @@ fn add_listeners_to_hierarchy(
}
}
-fn empty_listener(trigger: Trigger>) {
+fn empty_listener(trigger: On>) {
black_box(trigger);
}
diff --git a/benches/benches/bevy_ecs/observers/simple.rs b/benches/benches/bevy_ecs/observers/simple.rs
index 85207624e8..9c26b074e5 100644
--- a/benches/benches/bevy_ecs/observers/simple.rs
+++ b/benches/benches/bevy_ecs/observers/simple.rs
@@ -1,8 +1,8 @@
use core::hint::black_box;
use bevy_ecs::{
- event::Event,
- observer::{Trigger, TriggerTargets},
+ event::{EntityEvent, Event},
+ observer::{On, TriggerTargets},
world::World,
};
@@ -13,7 +13,7 @@ fn deterministic_rand() -> ChaCha8Rng {
ChaCha8Rng::seed_from_u64(42)
}
-#[derive(Clone, Event)]
+#[derive(Clone, Event, EntityEvent)]
struct EventBase;
pub fn observe_simple(criterion: &mut Criterion) {
@@ -46,7 +46,7 @@ pub fn observe_simple(criterion: &mut Criterion) {
group.finish();
}
-fn empty_listener_base(trigger: Trigger) {
+fn empty_listener_base(trigger: On) {
black_box(trigger);
}
diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs
index f8c46757dd..22b2f71f07 100644
--- a/crates/bevy_a11y/src/lib.rs
+++ b/crates/bevy_a11y/src/lib.rs
@@ -26,7 +26,8 @@ use accesskit::Node;
use bevy_app::Plugin;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
- prelude::{Component, Event},
+ component::Component,
+ event::{BufferedEvent, Event},
resource::Resource,
schedule::SystemSet,
};
@@ -44,7 +45,7 @@ use serde::{Deserialize, Serialize};
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
/// Wrapper struct for [`accesskit::ActionRequest`]. Required to allow it to be used as an `Event`.
-#[derive(Event, Deref, DerefMut)]
+#[derive(Event, BufferedEvent, Deref, DerefMut)]
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
pub struct ActionRequest(pub accesskit::ActionRequest);
diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml
index 9f9cd26587..9db4a97fd0 100644
--- a/crates/bevy_animation/Cargo.toml
+++ b/crates/bevy_animation/Cargo.toml
@@ -32,7 +32,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-fea
# other
petgraph = { version = "0.7", features = ["serde-1"] }
-ron = "0.8"
+ron = "0.10"
serde = "1"
blake3 = { version = "1.0" }
downcast-rs = { version = "2", default-features = false, features = ["std"] }
diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs
index aa6d252fee..a5f4041ac7 100644
--- a/crates/bevy_animation/src/graph.rs
+++ b/crates/bevy_animation/src/graph.rs
@@ -1,10 +1,11 @@
//! The animation graph, which allows animations to be blended together.
use core::{
+ fmt::Write,
iter,
ops::{Index, IndexMut, Range},
};
-use std::io::{self, Write};
+use std::io;
use bevy_asset::{
io::Reader, Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets, Handle, LoadContext,
diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs
index dd68595961..ae7ce42ed6 100644
--- a/crates/bevy_animation/src/lib.rs
+++ b/crates/bevy_animation/src/lib.rs
@@ -324,13 +324,13 @@ impl AnimationClip {
.push(variable_curve);
}
- /// Add a untargeted [`Event`] to this [`AnimationClip`].
+ /// Add an [`EntityEvent`] with no [`AnimationTarget`] to this [`AnimationClip`].
///
/// The `event` will be cloned and triggered on the [`AnimationPlayer`] entity once the `time` (in seconds)
/// is reached in the animation.
///
/// See also [`add_event_to_target`](Self::add_event_to_target).
- pub fn add_event(&mut self, time: f32, event: impl Event + Clone) {
+ pub fn add_event(&mut self, time: f32, event: impl EntityEvent + Clone) {
self.add_event_fn(
time,
move |commands: &mut Commands, entity: Entity, _time: f32, _weight: f32| {
@@ -339,7 +339,7 @@ impl AnimationClip {
);
}
- /// Add an [`Event`] to an [`AnimationTarget`] named by an [`AnimationTargetId`].
+ /// Add an [`EntityEvent`] to an [`AnimationTarget`] named by an [`AnimationTargetId`].
///
/// The `event` will be cloned and triggered on the entity matching the target once the `time` (in seconds)
/// is reached in the animation.
@@ -349,7 +349,7 @@ impl AnimationClip {
&mut self,
target_id: AnimationTargetId,
time: f32,
- event: impl Event + Clone,
+ event: impl EntityEvent + Clone,
) {
self.add_event_fn_to_target(
target_id,
@@ -360,19 +360,19 @@ impl AnimationClip {
);
}
- /// Add a untargeted event function to this [`AnimationClip`].
+ /// Add an event function with no [`AnimationTarget`] to this [`AnimationClip`].
///
/// The `func` will trigger on the [`AnimationPlayer`] entity once the `time` (in seconds)
/// is reached in the animation.
///
- /// For a simpler [`Event`]-based alternative, see [`AnimationClip::add_event`].
+ /// For a simpler [`EntityEvent`]-based alternative, see [`AnimationClip::add_event`].
/// See also [`add_event_to_target`](Self::add_event_to_target).
///
/// ```
/// # use bevy_animation::AnimationClip;
/// # let mut clip = AnimationClip::default();
/// clip.add_event_fn(1.0, |commands, entity, time, weight| {
- /// println!("Animation Event Triggered {entity:#?} at time {time} with weight {weight}");
+ /// println!("Animation event triggered {entity:#?} at time {time} with weight {weight}");
/// })
/// ```
pub fn add_event_fn(
@@ -388,14 +388,14 @@ impl AnimationClip {
/// The `func` will trigger on the entity matching the target once the `time` (in seconds)
/// is reached in the animation.
///
- /// For a simpler [`Event`]-based alternative, see [`AnimationClip::add_event_to_target`].
+ /// For a simpler [`EntityEvent`]-based alternative, see [`AnimationClip::add_event_to_target`].
/// Use [`add_event`](Self::add_event) instead if you don't have a specific target.
///
/// ```
/// # use bevy_animation::{AnimationClip, AnimationTargetId};
/// # let mut clip = AnimationClip::default();
/// clip.add_event_fn_to_target(AnimationTargetId::from_iter(["Arm", "Hand"]), 1.0, |commands, entity, time, weight| {
- /// println!("Animation Event Triggered {entity:#?} at time {time} with weight {weight}");
+ /// println!("Animation event triggered {entity:#?} at time {time} with weight {weight}");
/// })
/// ```
pub fn add_event_fn_to_target(
@@ -1534,7 +1534,7 @@ mod tests {
use super::*;
- #[derive(Event, Reflect, Clone)]
+ #[derive(Event, EntityEvent, Reflect, Clone)]
struct A;
#[track_caller]
diff --git a/crates/bevy_anti_aliasing/src/smaa/mod.rs b/crates/bevy_anti_aliasing/src/smaa/mod.rs
index 0947abc5f8..bb082c5a01 100644
--- a/crates/bevy_anti_aliasing/src/smaa/mod.rs
+++ b/crates/bevy_anti_aliasing/src/smaa/mod.rs
@@ -847,7 +847,7 @@ impl ViewNode for SmaaNode {
view_smaa_uniform_offset,
smaa_textures,
view_smaa_bind_groups,
- ): QueryItem<'w, Self::ViewQuery>,
+ ): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let pipeline_cache = world.resource::();
diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs
index 9f82b22073..fc16a499d1 100644
--- a/crates/bevy_app/src/app.rs
+++ b/crates/bevy_app/src/app.rs
@@ -106,6 +106,8 @@ impl Default for App {
#[cfg(feature = "bevy_reflect")]
{
+ use bevy_ecs::observer::ObservedBy;
+
#[cfg(not(feature = "reflect_auto_register"))]
app.init_resource::();
@@ -115,6 +117,7 @@ impl Default for App {
app.register_type::();
app.register_type::();
app.register_type::();
+ app.register_type::();
}
#[cfg(feature = "reflect_functions")]
@@ -346,7 +349,7 @@ impl App {
self
}
- /// Initializes `T` event handling by inserting an event queue resource ([`Events::`])
+ /// Initializes [`BufferedEvent`] handling for `T` by inserting an event queue resource ([`Events::`])
/// and scheduling an [`event_update_system`] in [`First`].
///
/// See [`Events`] for information on how to define events.
@@ -357,7 +360,7 @@ impl App {
/// # use bevy_app::prelude::*;
/// # use bevy_ecs::prelude::*;
/// #
- /// # #[derive(Event)]
+ /// # #[derive(Event, BufferedEvent)]
/// # struct MyEvent;
/// # let mut app = App::new();
/// #
@@ -365,7 +368,7 @@ impl App {
/// ```
pub fn add_event(&mut self) -> &mut Self
where
- T: Event,
+ T: BufferedEvent,
{
self.main_mut().add_event::();
self
@@ -1311,7 +1314,7 @@ impl App {
/// Spawns an [`Observer`] entity, which will watch for and respond to the given event.
///
- /// `observer` can be any system whose first parameter is a [`Trigger`].
+ /// `observer` can be any system whose first parameter is [`On`].
///
/// # Examples
///
@@ -1327,14 +1330,14 @@ impl App {
/// # friends_allowed: bool,
/// # };
/// #
- /// # #[derive(Event)]
+ /// # #[derive(Event, EntityEvent)]
/// # struct Invite;
/// #
/// # #[derive(Component)]
/// # struct Friend;
/// #
///
- /// app.add_observer(|trigger: Trigger, friends: Query>, mut commands: Commands| {
+ /// app.add_observer(|trigger: On, friends: Query>, mut commands: Commands| {
/// if trigger.event().friends_allowed {
/// for friend in friends.iter() {
/// commands.trigger_targets(Invite, friend);
@@ -1409,7 +1412,7 @@ fn run_once(mut app: App) -> AppExit {
app.should_exit().unwrap_or(AppExit::Success)
}
-/// An event that indicates the [`App`] should exit. If one or more of these are present at the end of an update,
+/// A [`BufferedEvent`] that indicates the [`App`] should exit. If one or more of these are present at the end of an update,
/// the [runner](App::set_runner) will end and ([maybe](App::run)) return control to the caller.
///
/// This event can be used to detect when an exit is requested. Make sure that systems listening
@@ -1419,7 +1422,7 @@ fn run_once(mut app: App) -> AppExit {
/// This type is roughly meant to map to a standard definition of a process exit code (0 means success, not 0 means error). Due to portability concerns
/// (see [`ExitCode`](https://doc.rust-lang.org/std/process/struct.ExitCode.html) and [`process::exit`](https://doc.rust-lang.org/std/process/fn.exit.html#))
/// we only allow error codes between 1 and [255](u8::MAX).
-#[derive(Event, Debug, Clone, Default, PartialEq, Eq)]
+#[derive(Event, BufferedEvent, Debug, Clone, Default, PartialEq, Eq)]
pub enum AppExit {
/// [`App`] exited without any problems.
#[default]
@@ -1487,9 +1490,9 @@ mod tests {
change_detection::{DetectChanges, ResMut},
component::Component,
entity::Entity,
- event::{Event, EventWriter, Events},
+ event::{BufferedEvent, Event, EventWriter, Events},
+ lifecycle::RemovedComponents,
query::With,
- removal_detection::RemovedComponents,
resource::Resource,
schedule::{IntoScheduleConfigs, ScheduleLabel},
system::{Commands, Query},
@@ -1853,7 +1856,7 @@ mod tests {
}
#[test]
fn events_should_be_updated_once_per_update() {
- #[derive(Event, Clone)]
+ #[derive(Event, BufferedEvent, Clone)]
struct TestEvent;
let mut app = App::new();
diff --git a/crates/bevy_app/src/propagate.rs b/crates/bevy_app/src/propagate.rs
index 5d766a626c..754ba3140e 100644
--- a/crates/bevy_app/src/propagate.rs
+++ b/crates/bevy_app/src/propagate.rs
@@ -6,9 +6,9 @@ use bevy_ecs::{
component::Component,
entity::Entity,
hierarchy::ChildOf,
+ lifecycle::RemovedComponents,
query::{Changed, Or, QueryFilter, With, Without},
relationship::{Relationship, RelationshipTarget},
- removal_detection::RemovedComponents,
schedule::{IntoScheduleConfigs, SystemSet},
system::{Commands, Local, Query},
};
diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs
index c340b80654..56d6b43d38 100644
--- a/crates/bevy_app/src/sub_app.rs
+++ b/crates/bevy_app/src/sub_app.rs
@@ -338,7 +338,7 @@ impl SubApp {
/// See [`App::add_event`].
pub fn add_event(&mut self) -> &mut Self
where
- T: Event,
+ T: BufferedEvent,
{
if !self.world.contains_resource::>() {
EventRegistry::register_event::(self.world_mut());
diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml
index cbb138b0f5..e91987f40a 100644
--- a/crates/bevy_asset/Cargo.toml
+++ b/crates/bevy_asset/Cargo.toml
@@ -54,7 +54,7 @@ parking_lot = { version = "0.12", default-features = false, features = [
"arc_lock",
"send_guard",
] }
-ron = { version = "0.8", default-features = false }
+ron = { version = "0.10", default-features = false }
serde = { version = "1", default-features = false, features = ["derive"] }
thiserror = { version = "2", default-features = false }
derive_more = { version = "1", default-features = false, features = ["from"] }
diff --git a/crates/bevy_asset/macros/src/lib.rs b/crates/bevy_asset/macros/src/lib.rs
index 443bd09ab9..a7ea87b752 100644
--- a/crates/bevy_asset/macros/src/lib.rs
+++ b/crates/bevy_asset/macros/src/lib.rs
@@ -1,6 +1,7 @@
-#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
+//! Macros for deriving asset traits.
+
use bevy_macro_utils::BevyManifest;
use proc_macro::{Span, TokenStream};
use quote::{format_ident, quote};
@@ -12,6 +13,7 @@ pub(crate) fn bevy_asset_path() -> Path {
const DEPENDENCY_ATTRIBUTE: &str = "dependency";
+/// Implement the `Asset` trait.
#[proc_macro_derive(Asset, attributes(dependency))]
pub fn derive_asset(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
@@ -30,6 +32,7 @@ pub fn derive_asset(input: TokenStream) -> TokenStream {
})
}
+/// Implement the `VisitAssetDependencies` trait.
#[proc_macro_derive(VisitAssetDependencies, attributes(dependency))]
pub fn derive_asset_dependency_visitor(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
diff --git a/crates/bevy_asset/src/asset_changed.rs b/crates/bevy_asset/src/asset_changed.rs
index d314fa3fd6..b43a8625e7 100644
--- a/crates/bevy_asset/src/asset_changed.rs
+++ b/crates/bevy_asset/src/asset_changed.rs
@@ -158,9 +158,9 @@ unsafe impl WorldQuery for AssetChanged {
fetch
}
- unsafe fn init_fetch<'w>(
+ unsafe fn init_fetch<'w, 's>(
world: UnsafeWorldCell<'w>,
- state: &Self::State,
+ state: &'s Self::State,
last_run: Tick,
this_run: Tick,
) -> Self::Fetch<'w> {
@@ -201,9 +201,9 @@ unsafe impl WorldQuery for AssetChanged {
const IS_DENSE: bool = <&A>::IS_DENSE;
- unsafe fn set_archetype<'w>(
+ unsafe fn set_archetype<'w, 's>(
fetch: &mut Self::Fetch<'w>,
- state: &Self::State,
+ state: &'s Self::State,
archetype: &'w Archetype,
table: &'w Table,
) {
@@ -215,7 +215,11 @@ unsafe impl WorldQuery for AssetChanged {
}
}
- unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) {
+ unsafe fn set_table<'w, 's>(
+ fetch: &mut Self::Fetch<'w>,
+ state: &Self::State,
+ table: &'w Table,
+ ) {
if let Some(inner) = &mut fetch.inner {
// SAFETY: We delegate to the inner `set_table` for `A`
unsafe {
@@ -265,6 +269,7 @@ unsafe impl QueryFilter for AssetChanged {
#[inline]
unsafe fn filter_fetch(
+ state: &Self::State,
fetch: &mut Self::Fetch<'_>,
entity: Entity,
table_row: TableRow,
@@ -272,7 +277,7 @@ unsafe impl QueryFilter for AssetChanged {
fetch.inner.as_mut().is_some_and(|inner| {
// SAFETY: We delegate to the inner `fetch` for `A`
unsafe {
- let handle = <&A>::fetch(inner, entity, table_row);
+ let handle = <&A>::fetch(&state.asset_id, inner, entity, table_row);
fetch.check.has_changed(handle)
}
})
diff --git a/crates/bevy_asset/src/event.rs b/crates/bevy_asset/src/event.rs
index 087cb44b5a..42de19fe44 100644
--- a/crates/bevy_asset/src/event.rs
+++ b/crates/bevy_asset/src/event.rs
@@ -1,12 +1,12 @@
use crate::{Asset, AssetId, AssetLoadError, AssetPath, UntypedAssetId};
-use bevy_ecs::event::Event;
+use bevy_ecs::event::{BufferedEvent, Event};
use bevy_reflect::Reflect;
use core::fmt::Debug;
-/// An event emitted when a specific [`Asset`] fails to load.
+/// A [`BufferedEvent`] emitted when a specific [`Asset`] fails to load.
///
/// For an untyped equivalent, see [`UntypedAssetLoadFailedEvent`].
-#[derive(Event, Clone, Debug)]
+#[derive(Event, BufferedEvent, Clone, Debug)]
pub struct AssetLoadFailedEvent {
/// The stable identifier of the asset that failed to load.
pub id: AssetId,
@@ -24,7 +24,7 @@ impl AssetLoadFailedEvent {
}
/// An untyped version of [`AssetLoadFailedEvent`].
-#[derive(Event, Clone, Debug)]
+#[derive(Event, BufferedEvent, Clone, Debug)]
pub struct UntypedAssetLoadFailedEvent {
/// The stable identifier of the asset that failed to load.
pub id: UntypedAssetId,
@@ -44,9 +44,9 @@ impl From<&AssetLoadFailedEvent> for UntypedAssetLoadFailedEvent {
}
}
-/// Events that occur for a specific loaded [`Asset`], such as "value changed" events and "dependency" events.
+/// [`BufferedEvent`]s that occur for a specific loaded [`Asset`], such as "value changed" events and "dependency" events.
#[expect(missing_docs, reason = "Documenting the id fields is unhelpful.")]
-#[derive(Event, Reflect)]
+#[derive(Event, BufferedEvent, Reflect)]
pub enum AssetEvent {
/// Emitted whenever an [`Asset`] is added.
Added { id: AssetId },
diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs
index 50bbfb5dfc..24405f0657 100644
--- a/crates/bevy_asset/src/loader.rs
+++ b/crates/bevy_asset/src/loader.rs
@@ -344,7 +344,7 @@ impl<'a> LoadContext<'a> {
/// Begins a new labeled asset load. Use the returned [`LoadContext`] to load
/// dependencies for the new asset and call [`LoadContext::finish`] to finalize the asset load.
- /// When finished, make sure you call [`LoadContext::add_labeled_asset`] to add the results back to the parent
+ /// When finished, make sure you call [`LoadContext::add_loaded_labeled_asset`] to add the results back to the parent
/// context.
/// Prefer [`LoadContext::labeled_asset_scope`] when possible, which will automatically add
/// the labeled [`LoadContext`] back to the parent context.
@@ -360,7 +360,7 @@ impl<'a> LoadContext<'a> {
/// # let load_context: LoadContext = panic!();
/// let mut handles = Vec::new();
/// for i in 0..2 {
- /// let mut labeled = load_context.begin_labeled_asset();
+ /// let labeled = load_context.begin_labeled_asset();
/// handles.push(std::thread::spawn(move || {
/// (i.to_string(), labeled.finish(Image::default()))
/// }));
@@ -385,7 +385,7 @@ impl<'a> LoadContext<'a> {
/// [`LoadedAsset`], which is registered under the `label` label.
///
/// This exists to remove the need to manually call [`LoadContext::begin_labeled_asset`] and then manually register the
- /// result with [`LoadContext::add_labeled_asset`].
+ /// result with [`LoadContext::add_loaded_labeled_asset`].
///
/// See [`AssetPath`] for more on labeled assets.
pub fn labeled_asset_scope(
diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs
index 5c436c1061..6c470891bd 100644
--- a/crates/bevy_asset/src/reflect.rs
+++ b/crates/bevy_asset/src/reflect.rs
@@ -18,16 +18,16 @@ pub struct ReflectAsset {
handle_type_id: TypeId,
assets_resource_type_id: TypeId,
- get: fn(&World, UntypedHandle) -> Option<&dyn Reflect>,
+ get: fn(&World, UntypedAssetId) -> Option<&dyn Reflect>,
// SAFETY:
// - may only be called with an [`UnsafeWorldCell`] which can be used to access the corresponding `Assets` resource mutably
// - may only be used to access **at most one** access at once
- get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedHandle) -> Option<&mut dyn Reflect>,
+ get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedAssetId) -> Option<&mut dyn Reflect>,
add: fn(&mut World, &dyn PartialReflect) -> UntypedHandle,
- insert: fn(&mut World, UntypedHandle, &dyn PartialReflect),
+ insert: fn(&mut World, UntypedAssetId, &dyn PartialReflect),
len: fn(&World) -> usize,
ids: for<'w> fn(&'w World) -> Box + 'w>,
- remove: fn(&mut World, UntypedHandle) -> Option>,
+ remove: fn(&mut World, UntypedAssetId) -> Option>,
}
impl ReflectAsset {
@@ -42,15 +42,19 @@ impl ReflectAsset {
}
/// Equivalent of [`Assets::get`]
- pub fn get<'w>(&self, world: &'w World, handle: UntypedHandle) -> Option<&'w dyn Reflect> {
- (self.get)(world, handle)
+ pub fn get<'w>(
+ &self,
+ world: &'w World,
+ asset_id: impl Into,
+ ) -> Option<&'w dyn Reflect> {
+ (self.get)(world, asset_id.into())
}
/// Equivalent of [`Assets::get_mut`]
pub fn get_mut<'w>(
&self,
world: &'w mut World,
- handle: UntypedHandle,
+ asset_id: impl Into,
) -> Option<&'w mut dyn Reflect> {
// SAFETY: unique world access
#[expect(
@@ -58,7 +62,7 @@ impl ReflectAsset {
reason = "Use of unsafe `Self::get_unchecked_mut()` function."
)]
unsafe {
- (self.get_unchecked_mut)(world.as_unsafe_world_cell(), handle)
+ (self.get_unchecked_mut)(world.as_unsafe_world_cell(), asset_id.into())
}
}
@@ -76,8 +80,8 @@ impl ReflectAsset {
/// # let handle_1: UntypedHandle = unimplemented!();
/// # let handle_2: UntypedHandle = unimplemented!();
/// let unsafe_world_cell = world.as_unsafe_world_cell();
- /// let a = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, handle_1).unwrap() };
- /// let b = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, handle_2).unwrap() };
+ /// let a = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, &handle_1).unwrap() };
+ /// let b = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, &handle_2).unwrap() };
/// // ^ not allowed, two mutable references through the same asset resource, even though the
/// // handles are distinct
///
@@ -96,10 +100,10 @@ impl ReflectAsset {
pub unsafe fn get_unchecked_mut<'w>(
&self,
world: UnsafeWorldCell<'w>,
- handle: UntypedHandle,
+ asset_id: impl Into,
) -> Option<&'w mut dyn Reflect> {
// SAFETY: requirements are deferred to the caller
- unsafe { (self.get_unchecked_mut)(world, handle) }
+ unsafe { (self.get_unchecked_mut)(world, asset_id.into()) }
}
/// Equivalent of [`Assets::add`]
@@ -107,13 +111,22 @@ impl ReflectAsset {
(self.add)(world, value)
}
/// Equivalent of [`Assets::insert`]
- pub fn insert(&self, world: &mut World, handle: UntypedHandle, value: &dyn PartialReflect) {
- (self.insert)(world, handle, value);
+ pub fn insert(
+ &self,
+ world: &mut World,
+ asset_id: impl Into,
+ value: &dyn PartialReflect,
+ ) {
+ (self.insert)(world, asset_id.into(), value);
}
/// Equivalent of [`Assets::remove`]
- pub fn remove(&self, world: &mut World, handle: UntypedHandle) -> Option> {
- (self.remove)(world, handle)
+ pub fn remove(
+ &self,
+ world: &mut World,
+ asset_id: impl Into,
+ ) -> Option> {
+ (self.remove)(world, asset_id.into())
}
/// Equivalent of [`Assets::len`]
@@ -137,17 +150,17 @@ impl FromType for ReflectAsset {
ReflectAsset {
handle_type_id: TypeId::of::>(),
assets_resource_type_id: TypeId::of::>(),
- get: |world, handle| {
+ get: |world, asset_id| {
let assets = world.resource::>();
- let asset = assets.get(&handle.typed_debug_checked());
+ let asset = assets.get(asset_id.typed_debug_checked());
asset.map(|asset| asset as &dyn Reflect)
},
- get_unchecked_mut: |world, handle| {
+ get_unchecked_mut: |world, asset_id| {
// SAFETY: `get_unchecked_mut` must be called with `UnsafeWorldCell` having access to `Assets`,
// and must ensure to only have at most one reference to it live at all times.
#[expect(unsafe_code, reason = "Uses `UnsafeWorldCell::get_resource_mut()`.")]
let assets = unsafe { world.get_resource_mut::>().unwrap().into_inner() };
- let asset = assets.get_mut(&handle.typed_debug_checked());
+ let asset = assets.get_mut(asset_id.typed_debug_checked());
asset.map(|asset| asset as &mut dyn Reflect)
},
add: |world, value| {
@@ -156,11 +169,11 @@ impl FromType for ReflectAsset {
.expect("could not call `FromReflect::from_reflect` in `ReflectAsset::add`");
assets.add(value).untyped()
},
- insert: |world, handle, value| {
+ insert: |world, asset_id, value| {
let mut assets = world.resource_mut::>();
let value: A = FromReflect::from_reflect(value)
.expect("could not call `FromReflect::from_reflect` in `ReflectAsset::set`");
- assets.insert(&handle.typed_debug_checked(), value);
+ assets.insert(asset_id.typed_debug_checked(), value);
},
len: |world| {
let assets = world.resource::>();
@@ -170,9 +183,9 @@ impl FromType for ReflectAsset {
let assets = world.resource::>();
Box::new(assets.ids().map(AssetId::untyped))
},
- remove: |world, handle| {
+ remove: |world, asset_id| {
let mut assets = world.resource_mut::>();
- let value = assets.remove(&handle.typed_debug_checked());
+ let value = assets.remove(asset_id.typed_debug_checked());
value.map(|value| Box::new(value) as Box)
},
}
@@ -200,7 +213,7 @@ impl FromType for ReflectAsset {
/// let reflect_asset = type_registry.get_type_data::(reflect_handle.asset_type_id()).unwrap();
///
/// let handle = reflect_handle.downcast_handle_untyped(handle.as_any()).unwrap();
-/// let value = reflect_asset.get(world, handle).unwrap();
+/// let value = reflect_asset.get(world, &handle).unwrap();
/// println!("{value:?}");
/// }
/// ```
@@ -247,7 +260,7 @@ mod tests {
use alloc::{string::String, vec::Vec};
use core::any::TypeId;
- use crate::{Asset, AssetApp, AssetPlugin, ReflectAsset, UntypedHandle};
+ use crate::{Asset, AssetApp, AssetPlugin, ReflectAsset};
use bevy_app::App;
use bevy_ecs::reflect::AppTypeRegistry;
use bevy_reflect::Reflect;
@@ -281,7 +294,7 @@ mod tests {
let handle = reflect_asset.add(app.world_mut(), &value);
// struct is a reserved keyword, so we can't use it here
let strukt = reflect_asset
- .get_mut(app.world_mut(), handle)
+ .get_mut(app.world_mut(), &handle)
.unwrap()
.reflect_mut()
.as_struct()
@@ -294,16 +307,12 @@ mod tests {
assert_eq!(reflect_asset.len(app.world()), 1);
let ids: Vec<_> = reflect_asset.ids(app.world()).collect();
assert_eq!(ids.len(), 1);
+ let id = ids[0];
- let fetched_handle = UntypedHandle::Weak(ids[0]);
- let asset = reflect_asset
- .get(app.world(), fetched_handle.clone_weak())
- .unwrap();
+ let asset = reflect_asset.get(app.world(), id).unwrap();
assert_eq!(asset.downcast_ref::().unwrap().field, "edited");
- reflect_asset
- .remove(app.world_mut(), fetched_handle)
- .unwrap();
+ reflect_asset.remove(app.world_mut(), id).unwrap();
assert_eq!(reflect_asset.len(app.world()), 0);
}
}
diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs
index 2b3898cd54..e120888616 100644
--- a/crates/bevy_asset/src/server/mod.rs
+++ b/crates/bevy_asset/src/server/mod.rs
@@ -1953,6 +1953,14 @@ impl AssetLoaderError {
pub fn path(&self) -> &AssetPath<'static> {
&self.path
}
+
+ /// The error the loader reported when attempting to load the asset.
+ ///
+ /// If you know the type of the error the asset loader returned, you can use
+ /// [`BevyError::downcast_ref()`] to get it.
+ pub fn error(&self) -> &BevyError {
+ &self.error
+ }
}
/// An error that occurs while resolving an asset added by `add_async`.
diff --git a/crates/bevy_audio/Cargo.toml b/crates/bevy_audio/Cargo.toml
index aff7f83b37..ae5385870d 100644
--- a/crates/bevy_audio/Cargo.toml
+++ b/crates/bevy_audio/Cargo.toml
@@ -19,12 +19,18 @@ bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" }
# other
+# TODO: Remove `coreaudio-sys` dep below when updating `cpal`.
rodio = { version = "0.20", default-features = false }
tracing = { version = "0.1", default-features = false, features = ["std"] }
[target.'cfg(target_os = "android")'.dependencies]
cpal = { version = "0.15", optional = true }
+[target.'cfg(target_vendor = "apple")'.dependencies]
+# NOTE: Explicitly depend on this patch version to fix:
+# https://github.com/bevyengine/bevy/issues/18893
+coreaudio-sys = { version = "0.2.17", default-features = false }
+
[target.'cfg(target_arch = "wasm32")'.dependencies]
# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption.
rodio = { version = "0.20", default-features = false, features = [
diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs
index b29fce72ac..1579519274 100644
--- a/crates/bevy_color/src/hsla.rs
+++ b/crates/bevy_color/src/hsla.rs
@@ -92,7 +92,7 @@ impl Hsla {
/// // Palette with 5 distinct hues
/// let palette = (0..5).map(Hsla::sequential_dispersed).collect::>();
/// ```
- pub fn sequential_dispersed(index: u32) -> Self {
+ pub const fn sequential_dispersed(index: u32) -> Self {
const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up
const RATIO_360: f32 = 360.0 / u32::MAX as f32;
diff --git a/crates/bevy_color/src/lcha.rs b/crates/bevy_color/src/lcha.rs
index e5f5ecab32..2a3b115bb3 100644
--- a/crates/bevy_color/src/lcha.rs
+++ b/crates/bevy_color/src/lcha.rs
@@ -96,7 +96,7 @@ impl Lcha {
/// // Palette with 5 distinct hues
/// let palette = (0..5).map(Lcha::sequential_dispersed).collect::>();
/// ```
- pub fn sequential_dispersed(index: u32) -> Self {
+ pub const fn sequential_dispersed(index: u32) -> Self {
const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up
const RATIO_360: f32 = 360.0 / u32::MAX as f32;
diff --git a/crates/bevy_color/src/oklcha.rs b/crates/bevy_color/src/oklcha.rs
index 91ffe422c7..ba52a519ae 100644
--- a/crates/bevy_color/src/oklcha.rs
+++ b/crates/bevy_color/src/oklcha.rs
@@ -92,7 +92,7 @@ impl Oklcha {
/// // Palette with 5 distinct hues
/// let palette = (0..5).map(Oklcha::sequential_dispersed).collect::>();
/// ```
- pub fn sequential_dispersed(index: u32) -> Self {
+ pub const fn sequential_dispersed(index: u32) -> Self {
const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up
const RATIO_360: f32 = 360.0 / u32::MAX as f32;
diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs
index 10ffdf9c63..65e51c8472 100644
--- a/crates/bevy_core_pipeline/src/bloom/mod.rs
+++ b/crates/bevy_core_pipeline/src/bloom/mod.rs
@@ -121,7 +121,7 @@ impl ViewNode for BloomNode {
bloom_settings,
upsampling_pipeline_ids,
downsampling_pipeline_ids,
- ): QueryItem<'w, Self::ViewQuery>,
+ ): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
if bloom_settings.intensity == 0.0 {
diff --git a/crates/bevy_core_pipeline/src/bloom/settings.rs b/crates/bevy_core_pipeline/src/bloom/settings.rs
index 195c2eb4c0..435ed037b5 100644
--- a/crates/bevy_core_pipeline/src/bloom/settings.rs
+++ b/crates/bevy_core_pipeline/src/bloom/settings.rs
@@ -227,7 +227,7 @@ impl ExtractComponent for Bloom {
type QueryFilter = With;
type Out = (Self, BloomUniforms);
- fn extract_component((bloom, camera): QueryItem<'_, Self::QueryData>) -> Option {
+ fn extract_component((bloom, camera): QueryItem<'_, '_, Self::QueryData>) -> Option {
match (
camera.physical_viewport_rect(),
camera.physical_viewport_size(),
diff --git a/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs
index 60f355c115..e8cd0c65c6 100644
--- a/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs
+++ b/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs
@@ -31,7 +31,7 @@ impl ViewNode for MainOpaquePass2dNode {
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
- (camera, view, target, depth): QueryItem<'w, Self::ViewQuery>,
+ (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let (Some(opaque_phases), Some(alpha_mask_phases)) = (
diff --git a/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs
index 494d4d0f89..4054283a57 100644
--- a/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs
+++ b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs
@@ -28,7 +28,7 @@ impl ViewNode for MainTransparentPass2dNode {
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
- (camera, view, target, depth): bevy_ecs::query::QueryItem<'w, Self::ViewQuery>,
+ (camera, view, target, depth): bevy_ecs::query::QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let Some(transparent_phases) =
diff --git a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs
index 3b1bc96c90..b19268ac1f 100644
--- a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs
+++ b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs
@@ -45,7 +45,7 @@ impl ViewNode for MainOpaquePass3dNode {
skybox_pipeline,
skybox_bind_group,
view_uniform_offset,
- ): QueryItem<'w, Self::ViewQuery>,
+ ): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let (Some(opaque_phases), Some(alpha_mask_phases)) = (
diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs
index ffac1eec6d..e786d2a222 100644
--- a/crates/bevy_core_pipeline/src/deferred/node.rs
+++ b/crates/bevy_core_pipeline/src/deferred/node.rs
@@ -36,7 +36,7 @@ impl ViewNode for EarlyDeferredGBufferPrepassNode {
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
- view_query: QueryItem<'w, Self::ViewQuery>,
+ view_query: QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
run_deferred_prepass(
@@ -74,7 +74,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode {
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
- view_query: QueryItem<'w, Self::ViewQuery>,
+ view_query: QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let (_, _, _, _, occlusion_culling, no_indirect_drawing) = view_query;
@@ -107,6 +107,7 @@ fn run_deferred_prepass<'w>(
render_context: &mut RenderContext<'w>,
(camera, extracted_view, view_depth_texture, view_prepass_textures, _, _): QueryItem<
'w,
+ '_,
::ViewQuery,
>,
is_late: bool,
diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs
index 38f5e1e796..c27d81180d 100644
--- a/crates/bevy_core_pipeline/src/dof/mod.rs
+++ b/crates/bevy_core_pipeline/src/dof/mod.rs
@@ -352,7 +352,7 @@ impl ViewNode for DepthOfFieldNode {
view_bind_group_layouts,
depth_of_field_uniform_index,
auxiliary_dof_texture,
- ): QueryItem<'w, Self::ViewQuery>,
+ ): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let pipeline_cache = world.resource::();
diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs
index 8dc51e4ed5..5f82e10599 100644
--- a/crates/bevy_core_pipeline/src/msaa_writeback.rs
+++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs
@@ -61,7 +61,7 @@ impl ViewNode for MsaaWritebackNode {
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
- (target, blit_pipeline_id, msaa): QueryItem<'w, Self::ViewQuery>,
+ (target, blit_pipeline_id, msaa): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
if *msaa == Msaa::Off {
diff --git a/crates/bevy_core_pipeline/src/oit/mod.rs b/crates/bevy_core_pipeline/src/oit/mod.rs
index 3ae95d71cc..5b5d038fa0 100644
--- a/crates/bevy_core_pipeline/src/oit/mod.rs
+++ b/crates/bevy_core_pipeline/src/oit/mod.rs
@@ -1,7 +1,7 @@
//! Order Independent Transparency (OIT) for 3d rendering. See [`OrderIndependentTransparencyPlugin`] for more details.
use bevy_app::prelude::*;
-use bevy_ecs::{component::*, prelude::*};
+use bevy_ecs::{component::*, lifecycle::ComponentHook, prelude::*};
use bevy_math::UVec2;
use bevy_platform::collections::HashSet;
use bevy_platform::time::Instant;
diff --git a/crates/bevy_core_pipeline/src/post_process/mod.rs b/crates/bevy_core_pipeline/src/post_process/mod.rs
index 1ab03c5dfa..0f188d1d73 100644
--- a/crates/bevy_core_pipeline/src/post_process/mod.rs
+++ b/crates/bevy_core_pipeline/src/post_process/mod.rs
@@ -352,7 +352,7 @@ impl ViewNode for PostProcessingNode {
&self,
_: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
- (view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, Self::ViewQuery>,
+ (view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let pipeline_cache = world.resource::();
@@ -485,7 +485,7 @@ impl ExtractComponent for ChromaticAberration {
type Out = ChromaticAberration;
fn extract_component(
- chromatic_aberration: QueryItem<'_, Self::QueryData>,
+ chromatic_aberration: QueryItem<'_, '_, Self::QueryData>,
) -> Option {
// Skip the postprocessing phase entirely if the intensity is zero.
if chromatic_aberration.intensity > 0.0 {
diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs
index deea2a5fa8..880e2b6892 100644
--- a/crates/bevy_core_pipeline/src/prepass/mod.rs
+++ b/crates/bevy_core_pipeline/src/prepass/mod.rs
@@ -74,11 +74,16 @@ pub struct MotionVectorPrepass;
#[reflect(Component, Default)]
pub struct DeferredPrepass;
+/// View matrices from the previous frame.
+///
+/// Useful for temporal rendering techniques that need access to last frame's camera data.
#[derive(Component, ShaderType, Clone)]
pub struct PreviousViewData {
pub view_from_world: Mat4,
pub clip_from_world: Mat4,
pub clip_from_view: Mat4,
+ pub world_from_clip: Mat4,
+ pub view_from_clip: Mat4,
}
#[derive(Resource, Default)]
diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs
index 04cc1890b0..500cc0a42b 100644
--- a/crates/bevy_core_pipeline/src/prepass/node.rs
+++ b/crates/bevy_core_pipeline/src/prepass/node.rs
@@ -36,7 +36,7 @@ impl ViewNode for EarlyPrepassNode {
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
- view_query: QueryItem<'w, Self::ViewQuery>,
+ view_query: QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
run_prepass(graph, render_context, view_query, world, "early prepass")
@@ -73,7 +73,7 @@ impl ViewNode for LatePrepassNode {
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
- query: QueryItem<'w, Self::ViewQuery>,
+ query: QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
// We only need a late prepass if we have occlusion culling and indirect
@@ -112,7 +112,7 @@ fn run_prepass<'w>(
_,
_,
has_deferred,
- ): QueryItem<'w, ::ViewQuery>,
+ ): QueryItem<'w, '_, ::ViewQuery>,
world: &'w World,
label: &'static str,
) -> Result<(), NodeRunError> {
diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs
index cb75df2053..51c6934ece 100644
--- a/crates/bevy_core_pipeline/src/skybox/mod.rs
+++ b/crates/bevy_core_pipeline/src/skybox/mod.rs
@@ -113,7 +113,9 @@ impl ExtractComponent for Skybox {
type QueryFilter = ();
type Out = (Self, SkyboxUniforms);
- fn extract_component((skybox, exposure): QueryItem<'_, Self::QueryData>) -> Option {
+ fn extract_component(
+ (skybox, exposure): QueryItem<'_, '_, Self::QueryData>,
+ ) -> Option {
let exposure = exposure
.map(Exposure::exposure)
.unwrap_or_else(|| Exposure::default().exposure());
@@ -123,7 +125,7 @@ impl ExtractComponent for Skybox {
SkyboxUniforms {
brightness: skybox.brightness * exposure,
transform: Transform::from_rotation(skybox.rotation)
- .compute_matrix()
+ .to_matrix()
.inverse(),
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_wasm_padding_8b: 0,
diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml
new file mode 100644
index 0000000000..83ecd67ede
--- /dev/null
+++ b/crates/bevy_core_widgets/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "bevy_core_widgets"
+version = "0.16.0-dev"
+edition = "2024"
+description = "Unstyled common widgets for 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_log = { path = "../bevy_log", version = "0.16.0-dev" }
+bevy_math = { path = "../bevy_math", version = "0.16.0-dev" }
+bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" }
+bevy_render = { path = "../bevy_render", version = "0.16.0-dev" }
+bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" }
+bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev", features = [
+ "bevy_ui_picking_backend",
+] }
+
+# other
+accesskit = "0.19"
+
+[features]
+default = []
+
+[lints]
+workspace = true
+
+[package.metadata.docs.rs]
+rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
+all-features = true
diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs
new file mode 100644
index 0000000000..ec30b625f9
--- /dev/null
+++ b/crates/bevy_core_widgets/src/core_button.rs
@@ -0,0 +1,141 @@
+use accesskit::Role;
+use bevy_a11y::AccessibilityNode;
+use bevy_app::{App, Plugin};
+use bevy_ecs::query::Has;
+use bevy_ecs::system::ResMut;
+use bevy_ecs::{
+ component::Component,
+ entity::Entity,
+ observer::On,
+ query::With,
+ system::{Commands, Query, SystemId},
+};
+use bevy_input::keyboard::{KeyCode, KeyboardInput};
+use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible};
+use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release};
+use bevy_ui::{InteractionDisabled, Pressed};
+
+/// Headless button widget. This widget maintains a "pressed" state, which is used to
+/// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked`
+/// event when the button is un-pressed.
+#[derive(Component, Debug)]
+#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))]
+pub struct CoreButton {
+ /// Optional system to run when the button is clicked, or when the Enter or Space key
+ /// is pressed while the button is focused. If this field is `None`, the button will
+ /// emit a `ButtonClicked` event when clicked.
+ pub on_click: Option,
+}
+
+fn button_on_key_event(
+ mut trigger: On>,
+ q_state: Query<(&CoreButton, Has)>,
+ mut commands: Commands,
+) {
+ if let Ok((bstate, disabled)) = q_state.get(trigger.target()) {
+ 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: On>,
+ mut q_state: Query<(&CoreButton, Has, Has)>,
+ mut commands: Commands,
+) {
+ if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target()) {
+ 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: On>,
+ mut q_state: Query<(Entity, Has, Has), With>,
+ focus: Option>,
+ focus_visible: Option>,
+ mut commands: Commands,
+) {
+ if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) {
+ 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() != Entity::PLACEHOLDER).then_some(trigger.target());
+ }
+ if let Some(mut focus_visible) = focus_visible {
+ focus_visible.0 = false;
+ }
+ }
+ }
+}
+
+fn button_on_pointer_up(
+ mut trigger: On>,
+ mut q_state: Query<(Entity, Has, Has), With>,
+ mut commands: Commands,
+) {
+ if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) {
+ trigger.propagate(false);
+ if !disabled && pressed {
+ commands.entity(button).remove::();
+ }
+ }
+}
+
+fn button_on_pointer_drag_end(
+ mut trigger: On>,
+ mut q_state: Query<(Entity, Has, Has), With>,
+ mut commands: Commands,
+) {
+ if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) {
+ trigger.propagate(false);
+ if !disabled && pressed {
+ commands.entity(button).remove::();
+ }
+ }
+}
+
+fn button_on_pointer_cancel(
+ mut trigger: On>,
+ mut q_state: Query<(Entity, Has, Has), With>,
+ mut commands: Commands,
+) {
+ if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) {
+ trigger.propagate(false);
+ if !disabled && pressed {
+ commands.entity(button).remove::();
+ }
+ }
+}
+
+/// Plugin that adds the observers for the [`CoreButton`] widget.
+pub struct CoreButtonPlugin;
+
+impl Plugin for CoreButtonPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_observer(button_on_key_event)
+ .add_observer(button_on_pointer_down)
+ .add_observer(button_on_pointer_up)
+ .add_observer(button_on_pointer_click)
+ .add_observer(button_on_pointer_drag_end)
+ .add_observer(button_on_pointer_cancel);
+ }
+}
diff --git a/crates/bevy_core_widgets/src/core_checkbox.rs b/crates/bevy_core_widgets/src/core_checkbox.rs
new file mode 100644
index 0000000000..fc12811055
--- /dev/null
+++ b/crates/bevy_core_widgets/src/core_checkbox.rs
@@ -0,0 +1,179 @@
+use accesskit::Role;
+use bevy_a11y::AccessibilityNode;
+use bevy_app::{App, Plugin};
+use bevy_ecs::event::{EntityEvent, Event};
+use bevy_ecs::query::{Has, Without};
+use bevy_ecs::system::{In, ResMut};
+use bevy_ecs::{
+ component::Component,
+ observer::On,
+ system::{Commands, Query, SystemId},
+};
+use bevy_input::keyboard::{KeyCode, KeyboardInput};
+use bevy_input::ButtonState;
+use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible};
+use bevy_picking::events::{Click, Pointer};
+use bevy_ui::{Checkable, Checked, InteractionDisabled};
+
+/// Headless widget implementation for checkboxes. The [`Checked`] component represents the current
+/// state of the checkbox. The `on_change` field is an optional system id that will be run when the
+/// checkbox is clicked, or when the `Enter` or `Space` key is pressed while the checkbox is
+/// focused. If the `on_change` field is `None`, then instead of calling a callback, the checkbox
+/// will update its own [`Checked`] state directly.
+///
+/// # Toggle switches
+///
+/// The [`CoreCheckbox`] component can be used to implement other kinds of toggle widgets. If you
+/// are going to do a toggle switch, you should override the [`AccessibilityNode`] component with
+/// the `Switch` role instead of the `Checkbox` role.
+#[derive(Component, Debug, Default)]
+#[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)]
+pub struct CoreCheckbox {
+ /// One-shot system that is run when the checkbox state needs to be changed.
+ pub on_change: Option>>,
+}
+
+fn checkbox_on_key_input(
+ mut ev: On>,
+ q_checkbox: Query<(&CoreCheckbox, Has), Without>,
+ mut commands: Commands,
+) {
+ if let Ok((checkbox, is_checked)) = q_checkbox.get(ev.target()) {
+ let event = &ev.event().input;
+ if event.state == ButtonState::Pressed
+ && !event.repeat
+ && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space)
+ {
+ ev.propagate(false);
+ set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked);
+ }
+ }
+}
+
+fn checkbox_on_pointer_click(
+ mut ev: On>,
+ q_checkbox: Query<(&CoreCheckbox, Has, Has)>,
+ focus: Option>,
+ focus_visible: Option>,
+ mut commands: Commands,
+) {
+ if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) {
+ // 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 = Some(ev.target());
+ }
+ if let Some(mut focus_visible) = focus_visible {
+ focus_visible.0 = false;
+ }
+
+ ev.propagate(false);
+ if !disabled {
+ set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked);
+ }
+ }
+}
+
+/// Event which can be triggered on a checkbox to set the checked state. This can be used to control
+/// the checkbox via gamepad buttons or other inputs.
+///
+/// # Example:
+///
+/// ```
+/// use bevy_ecs::system::Commands;
+/// use bevy_core_widgets::{CoreCheckbox, SetChecked};
+///
+/// fn setup(mut commands: Commands) {
+/// // Create a checkbox
+/// let checkbox = commands.spawn((
+/// CoreCheckbox::default(),
+/// )).id();
+///
+/// // Set to checked
+/// commands.trigger_targets(SetChecked(true), checkbox);
+/// }
+/// ```
+#[derive(Event, EntityEvent)]
+pub struct SetChecked(pub bool);
+
+/// Event which can be triggered on a checkbox to toggle the checked state. This can be used to
+/// control the checkbox via gamepad buttons or other inputs.
+///
+/// # Example:
+///
+/// ```
+/// use bevy_ecs::system::Commands;
+/// use bevy_core_widgets::{CoreCheckbox, ToggleChecked};
+///
+/// fn setup(mut commands: Commands) {
+/// // Create a checkbox
+/// let checkbox = commands.spawn((
+/// CoreCheckbox::default(),
+/// )).id();
+///
+/// // Set to checked
+/// commands.trigger_targets(ToggleChecked, checkbox);
+/// }
+/// ```
+#[derive(Event, EntityEvent)]
+pub struct ToggleChecked;
+
+fn checkbox_on_set_checked(
+ mut ev: On,
+ q_checkbox: Query<(&CoreCheckbox, Has, Has)>,
+ mut commands: Commands,
+) {
+ if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) {
+ ev.propagate(false);
+ if disabled {
+ return;
+ }
+
+ let will_be_checked = ev.event().0;
+ if will_be_checked != is_checked {
+ set_checkbox_state(&mut commands, ev.target(), checkbox, will_be_checked);
+ }
+ }
+}
+
+fn checkbox_on_toggle_checked(
+ mut ev: On,
+ q_checkbox: Query<(&CoreCheckbox, Has, Has)>,
+ mut commands: Commands,
+) {
+ if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) {
+ ev.propagate(false);
+ if disabled {
+ return;
+ }
+
+ set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked);
+ }
+}
+
+fn set_checkbox_state(
+ commands: &mut Commands,
+ entity: impl Into,
+ checkbox: &CoreCheckbox,
+ new_state: bool,
+) {
+ if let Some(on_change) = checkbox.on_change {
+ commands.run_system_with(on_change, new_state);
+ } else if new_state {
+ commands.entity(entity.into()).insert(Checked);
+ } else {
+ commands.entity(entity.into()).remove::();
+ }
+}
+
+/// Plugin that adds the observers for the [`CoreCheckbox`] widget.
+pub struct CoreCheckboxPlugin;
+
+impl Plugin for CoreCheckboxPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_observer(checkbox_on_key_input)
+ .add_observer(checkbox_on_pointer_click)
+ .add_observer(checkbox_on_set_checked)
+ .add_observer(checkbox_on_toggle_checked);
+ }
+}
diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs
new file mode 100644
index 0000000000..ecd6d52fbe
--- /dev/null
+++ b/crates/bevy_core_widgets/src/core_slider.rs
@@ -0,0 +1,514 @@
+use core::ops::RangeInclusive;
+
+use accesskit::{Orientation, Role};
+use bevy_a11y::AccessibilityNode;
+use bevy_app::{App, Plugin};
+use bevy_ecs::entity::Entity;
+use bevy_ecs::event::{EntityEvent, Event};
+use bevy_ecs::hierarchy::{ChildOf, Children};
+use bevy_ecs::lifecycle::Insert;
+use bevy_ecs::query::Has;
+use bevy_ecs::system::{In, Res, ResMut};
+use bevy_ecs::world::DeferredWorld;
+use bevy_ecs::{
+ component::Component,
+ observer::On,
+ query::With,
+ system::{Commands, Query, SystemId},
+};
+use bevy_input::keyboard::{KeyCode, KeyboardInput};
+use bevy_input::ButtonState;
+use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible};
+use bevy_log::warn_once;
+use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press};
+use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale};
+
+/// Defines how the slider should behave when you click on the track (not the thumb).
+#[derive(Debug, Default)]
+pub enum TrackClick {
+ /// Clicking on the track lets you drag to edit the value, just like clicking on the thumb.
+ #[default]
+ Drag,
+ /// Clicking on the track increments or decrements the slider by [`SliderStep`].
+ Step,
+ /// Clicking on the track snaps the value to the clicked position.
+ Snap,
+}
+
+/// A headless slider widget, which can be used to build custom sliders. Sliders have a value
+/// (represented by the [`SliderValue`] component) and a range (represented by [`SliderRange`]). An
+/// optional step size can be specified via [`SliderStep`].
+///
+/// You can also control the slider remotely by triggering a [`SetSliderValue`] event on it. This
+/// can be useful in a console environment for controlling the value gamepad inputs.
+///
+/// The presence of the `on_change` property controls whether the slider uses internal or external
+/// state management. If the `on_change` property is `None`, then the slider updates its own state
+/// automatically. Otherwise, the `on_change` property contains the id of a one-shot system which is
+/// passed the new slider value. In this case, the slider value is not modified, it is the
+/// responsibility of the callback to trigger whatever data-binding mechanism is used to update the
+/// slider's value.
+///
+/// Typically a slider will contain entities representing the "track" and "thumb" elements. The core
+/// slider makes no assumptions about the hierarchical structure of these elements, but expects that
+/// the thumb will be marked with a [`CoreSliderThumb`] component.
+///
+/// The core slider does not modify the visible position of the thumb: that is the responsibility of
+/// the stylist. This can be done either in percent or pixel units as desired. To prevent overhang
+/// at the ends of the slider, the positioning should take into account the thumb width, by reducing
+/// the amount of travel. So for example, in a slider 100px wide, with a thumb that is 10px, the
+/// amount of travel is 90px. The core slider's calculations for clicking and dragging assume this
+/// is the case, and will reduce the travel by the measured size of the thumb entity, which allows
+/// the movement of the thumb to be perfectly synchronized with the movement of the mouse.
+///
+/// In cases where overhang is desired for artistic reasons, the thumb may have additional
+/// decorative child elements, absolutely positioned, which don't affect the size measurement.
+#[derive(Component, Debug, Default)]
+#[require(
+ AccessibilityNode(accesskit::Node::new(Role::Slider)),
+ CoreSliderDragState,
+ SliderValue,
+ SliderRange,
+ SliderStep
+)]
+pub struct CoreSlider {
+ /// Callback which is called when the slider is dragged or the value is changed via other user
+ /// interaction. If this value is `None`, then the slider will self-update.
+ pub on_change: Option>>,
+ /// Set the track-clicking behavior for this slider.
+ pub track_click: TrackClick,
+ // TODO: Think about whether we want a "vertical" option.
+}
+
+/// Marker component that identifies which descendant element is the slider thumb.
+#[derive(Component, Debug, Default)]
+pub struct CoreSliderThumb;
+
+/// A component which stores the current value of the slider.
+#[derive(Component, Debug, Default, PartialEq, Clone, Copy)]
+#[component(immutable)]
+pub struct SliderValue(pub f32);
+
+/// A component which represents the allowed range of the slider value. Defaults to 0.0..=1.0.
+#[derive(Component, Debug, PartialEq, Clone, Copy)]
+#[component(immutable)]
+pub struct SliderRange {
+ start: f32,
+ end: f32,
+}
+
+impl SliderRange {
+ /// Creates a new slider range with the given start and end values.
+ pub fn new(start: f32, end: f32) -> Self {
+ if end < start {
+ warn_once!(
+ "Expected SliderRange::start ({}) <= SliderRange::end ({})",
+ start,
+ end
+ );
+ }
+ Self { start, end }
+ }
+
+ /// Creates a new slider range from a Rust range.
+ pub fn from_range(range: RangeInclusive) -> Self {
+ let (start, end) = range.into_inner();
+ Self { start, end }
+ }
+
+ /// Returns the minimum allowed value for this slider.
+ pub fn start(&self) -> f32 {
+ self.start
+ }
+
+ /// Return a new instance of a `SliderRange` with a new start position.
+ pub fn with_start(&self, start: f32) -> Self {
+ Self::new(start, self.end)
+ }
+
+ /// Returns the maximum allowed value for this slider.
+ pub fn end(&self) -> f32 {
+ self.end
+ }
+
+ /// Return a new instance of a `SliderRange` with a new end position.
+ pub fn with_end(&self, end: f32) -> Self {
+ Self::new(self.start, end)
+ }
+
+ /// Returns the full span of the range (max - min).
+ pub fn span(&self) -> f32 {
+ self.end - self.start
+ }
+
+ /// Returns the center value of the range.
+ pub fn center(&self) -> f32 {
+ (self.start + self.end) / 2.0
+ }
+
+ /// Constrain a value between the minimum and maximum allowed values for this slider.
+ pub fn clamp(&self, value: f32) -> f32 {
+ value.clamp(self.start, self.end)
+ }
+
+ /// Compute the position of the thumb on the slider, as a value between 0 and 1, taking
+ /// into account the proportion of the value between the minimum and maximum limits.
+ pub fn thumb_position(&self, value: f32) -> f32 {
+ if self.end > self.start {
+ (value - self.start) / (self.end - self.start)
+ } else {
+ 0.5
+ }
+ }
+}
+
+impl Default for SliderRange {
+ fn default() -> Self {
+ Self {
+ start: 0.0,
+ end: 1.0,
+ }
+ }
+}
+
+/// Defines the amount by which to increment or decrement the slider value when using keyboard
+/// shorctuts. Defaults to 1.0.
+#[derive(Component, Debug, PartialEq, Clone)]
+#[component(immutable)]
+pub struct SliderStep(pub f32);
+
+impl Default for SliderStep {
+ fn default() -> Self {
+ Self(1.0)
+ }
+}
+
+/// Component used to manage the state of a slider during dragging.
+#[derive(Component, Default)]
+pub struct CoreSliderDragState {
+ /// Whether the slider is currently being dragged.
+ pub dragging: bool,
+
+ /// The value of the slider when dragging started.
+ offset: f32,
+}
+
+pub(crate) fn slider_on_pointer_down(
+ mut trigger: On>,
+ q_slider: Query<(
+ &CoreSlider,
+ &SliderValue,
+ &SliderRange,
+ &SliderStep,
+ &ComputedNode,
+ &ComputedNodeTarget,
+ &UiGlobalTransform,
+ Has,
+ )>,
+ q_thumb: Query<&ComputedNode, With>,
+ q_children: Query<&Children>,
+ q_parents: Query<&ChildOf>,
+ focus: Option>,
+ focus_visible: Option>,
+ mut commands: Commands,
+ ui_scale: Res,
+) {
+ if q_thumb.contains(trigger.target()) {
+ // Thumb click, stop propagation to prevent track click.
+ trigger.propagate(false);
+
+ // Find the slider entity that's an ancestor of the thumb
+ if let Some(slider_entity) = q_parents
+ .iter_ancestors(trigger.target())
+ .find(|entity| q_slider.contains(*entity))
+ {
+ // Set focus to slider and hide focus ring
+ if let Some(mut focus) = focus {
+ focus.0 = Some(slider_entity);
+ }
+ if let Some(mut focus_visible) = focus_visible {
+ focus_visible.0 = false;
+ }
+ }
+ } else if let Ok((slider, value, range, step, node, node_target, transform, disabled)) =
+ q_slider.get(trigger.target())
+ {
+ // Track click
+ trigger.propagate(false);
+
+ // Set focus to slider and hide focus ring
+ if let Some(mut focus) = focus {
+ focus.0 = (trigger.target() != Entity::PLACEHOLDER).then_some(trigger.target());
+ }
+ if let Some(mut focus_visible) = focus_visible {
+ focus_visible.0 = false;
+ }
+
+ if disabled {
+ return;
+ }
+
+ // Find thumb size by searching descendants for the first entity with CoreSliderThumb
+ let thumb_size = q_children
+ .iter_descendants(trigger.target())
+ .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x))
+ .unwrap_or(0.0);
+
+ // Detect track click.
+ let local_pos = transform.try_inverse().unwrap().transform_point2(
+ trigger.event().pointer_location.position * node_target.scale_factor() / ui_scale.0,
+ );
+ let track_width = node.size().x - thumb_size;
+ // Avoid division by zero
+ let click_val = if track_width > 0. {
+ local_pos.x * range.span() / track_width + range.center()
+ } else {
+ 0.
+ };
+
+ // Compute new value from click position
+ let new_value = range.clamp(match slider.track_click {
+ TrackClick::Drag => {
+ return;
+ }
+ TrackClick::Step => {
+ if click_val < value.0 {
+ value.0 - step.0
+ } else {
+ value.0 + step.0
+ }
+ }
+ TrackClick::Snap => click_val,
+ });
+
+ if let Some(on_change) = slider.on_change {
+ commands.run_system_with(on_change, new_value);
+ } else {
+ commands
+ .entity(trigger.target())
+ .insert(SliderValue(new_value));
+ }
+ }
+}
+
+pub(crate) fn slider_on_drag_start(
+ mut trigger: On>,
+ mut q_slider: Query<
+ (
+ &SliderValue,
+ &mut CoreSliderDragState,
+ Has,
+ ),
+ With,
+ >,
+) {
+ if let Ok((value, mut drag, disabled)) = q_slider.get_mut(trigger.target()) {
+ trigger.propagate(false);
+ if !disabled {
+ drag.dragging = true;
+ drag.offset = value.0;
+ }
+ }
+}
+
+pub(crate) fn slider_on_drag(
+ mut trigger: On>,
+ mut q_slider: Query<(
+ &ComputedNode,
+ &CoreSlider,
+ &SliderRange,
+ &UiGlobalTransform,
+ &mut CoreSliderDragState,
+ Has,
+ )>,
+ q_thumb: Query<&ComputedNode, With>,
+ q_children: Query<&Children>,
+ mut commands: Commands,
+ ui_scale: Res,
+) {
+ if let Ok((node, slider, range, transform, drag, disabled)) = q_slider.get_mut(trigger.target())
+ {
+ trigger.propagate(false);
+ if drag.dragging && !disabled {
+ let mut distance = trigger.event().distance / ui_scale.0;
+ distance.y *= -1.;
+ let distance = transform.transform_vector2(distance);
+ // Find thumb size by searching descendants for the first entity with CoreSliderThumb
+ let thumb_size = q_children
+ .iter_descendants(trigger.target())
+ .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x))
+ .unwrap_or(0.0);
+ let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0);
+ let span = range.span();
+ let new_value = if span > 0. {
+ range.clamp(drag.offset + (distance.x * span) / slider_width)
+ } else {
+ range.start() + span * 0.5
+ };
+
+ if let Some(on_change) = slider.on_change {
+ commands.run_system_with(on_change, new_value);
+ } else {
+ commands
+ .entity(trigger.target())
+ .insert(SliderValue(new_value));
+ }
+ }
+ }
+}
+
+pub(crate) fn slider_on_drag_end(
+ mut trigger: On>,
+ mut q_slider: Query<(&CoreSlider, &mut CoreSliderDragState)>,
+) {
+ if let Ok((_slider, mut drag)) = q_slider.get_mut(trigger.target()) {
+ trigger.propagate(false);
+ if drag.dragging {
+ drag.dragging = false;
+ }
+ }
+}
+
+fn slider_on_key_input(
+ mut trigger: On>,
+ q_slider: Query<(
+ &CoreSlider,
+ &SliderValue,
+ &SliderRange,
+ &SliderStep,
+ Has,
+ )>,
+ mut commands: Commands,
+) {
+ if let Ok((slider, value, range, step, disabled)) = q_slider.get(trigger.target()) {
+ let event = &trigger.event().input;
+ if !disabled && event.state == ButtonState::Pressed {
+ let new_value = match event.key_code {
+ KeyCode::ArrowLeft => range.clamp(value.0 - step.0),
+ KeyCode::ArrowRight => range.clamp(value.0 + step.0),
+ KeyCode::Home => range.start(),
+ KeyCode::End => range.end(),
+ _ => {
+ return;
+ }
+ };
+ trigger.propagate(false);
+ if let Some(on_change) = slider.on_change {
+ commands.run_system_with(on_change, new_value);
+ } else {
+ commands
+ .entity(trigger.target())
+ .insert(SliderValue(new_value));
+ }
+ }
+ }
+}
+
+pub(crate) fn slider_on_insert(trigger: On, mut world: DeferredWorld) {
+ let mut entity = world.entity_mut(trigger.target());
+ if let Some(mut accessibility) = entity.get_mut::() {
+ accessibility.set_orientation(Orientation::Horizontal);
+ }
+}
+
+pub(crate) fn slider_on_insert_value(trigger: On, mut world: DeferredWorld) {
+ let mut entity = world.entity_mut(trigger.target());
+ let value = entity.get::().unwrap().0;
+ if let Some(mut accessibility) = entity.get_mut::() {
+ accessibility.set_numeric_value(value.into());
+ }
+}
+
+pub(crate) fn slider_on_insert_range(trigger: On, mut world: DeferredWorld) {
+ let mut entity = world.entity_mut(trigger.target());
+ let range = *entity.get::().unwrap();
+ if let Some(mut accessibility) = entity.get_mut::() {
+ accessibility.set_min_numeric_value(range.start().into());
+ accessibility.set_max_numeric_value(range.end().into());
+ }
+}
+
+pub(crate) fn slider_on_insert_step(trigger: On, mut world: DeferredWorld) {
+ let mut entity = world.entity_mut(trigger.target());
+ let step = entity.get::().unwrap().0;
+ if let Some(mut accessibility) = entity.get_mut::() {
+ accessibility.set_numeric_value_step(step.into());
+ }
+}
+
+/// An [`EntityEvent`] that can be triggered on a slider to modify its value (using the `on_change` callback).
+/// This can be used to control the slider via gamepad buttons or other inputs. The value will be
+/// clamped when the event is processed.
+///
+/// # Example:
+///
+/// ```
+/// use bevy_ecs::system::Commands;
+/// use bevy_core_widgets::{CoreSlider, SliderRange, SliderValue, SetSliderValue};
+///
+/// fn setup(mut commands: Commands) {
+/// // Create a slider
+/// let slider = commands.spawn((
+/// CoreSlider::default(),
+/// SliderValue(0.5),
+/// SliderRange::new(0.0, 1.0),
+/// )).id();
+///
+/// // Set to an absolute value
+/// commands.trigger_targets(SetSliderValue::Absolute(0.75), slider);
+///
+/// // Adjust relatively
+/// commands.trigger_targets(SetSliderValue::Relative(-0.25), slider);
+/// }
+/// ```
+#[derive(Event, EntityEvent, Clone)]
+pub enum SetSliderValue {
+ /// Set the slider value to a specific value.
+ Absolute(f32),
+ /// Add a delta to the slider value.
+ Relative(f32),
+ /// Add a delta to the slider value, multiplied by the step size.
+ RelativeStep(f32),
+}
+
+fn slider_on_set_value(
+ mut trigger: On,
+ q_slider: Query<(&CoreSlider, &SliderValue, &SliderRange, Option<&SliderStep>)>,
+ mut commands: Commands,
+) {
+ if let Ok((slider, value, range, step)) = q_slider.get(trigger.target()) {
+ trigger.propagate(false);
+ let new_value = match trigger.event() {
+ SetSliderValue::Absolute(new_value) => range.clamp(*new_value),
+ SetSliderValue::Relative(delta) => range.clamp(value.0 + *delta),
+ SetSliderValue::RelativeStep(delta) => {
+ range.clamp(value.0 + *delta * step.map(|s| s.0).unwrap_or_default())
+ }
+ };
+ if let Some(on_change) = slider.on_change {
+ commands.run_system_with(on_change, new_value);
+ } else {
+ commands
+ .entity(trigger.target())
+ .insert(SliderValue(new_value));
+ }
+ }
+}
+
+/// Plugin that adds the observers for the [`CoreSlider`] widget.
+pub struct CoreSliderPlugin;
+
+impl Plugin for CoreSliderPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_observer(slider_on_pointer_down)
+ .add_observer(slider_on_drag_start)
+ .add_observer(slider_on_drag_end)
+ .add_observer(slider_on_drag)
+ .add_observer(slider_on_key_input)
+ .add_observer(slider_on_insert)
+ .add_observer(slider_on_insert_value)
+ .add_observer(slider_on_insert_range)
+ .add_observer(slider_on_insert_step)
+ .add_observer(slider_on_set_value);
+ }
+}
diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs
new file mode 100644
index 0000000000..cdb9142b52
--- /dev/null
+++ b/crates/bevy_core_widgets/src/lib.rs
@@ -0,0 +1,38 @@
+//! 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.
+
+// Note on naming: the `Core` prefix is used on components that would normally be internal to the
+// styled/opinionated widgets that use them. Components which are directly exposed to users above
+// the widget level, like `SliderValue`, should not have the `Core` prefix.
+
+mod core_button;
+mod core_checkbox;
+mod core_slider;
+
+use bevy_app::{App, Plugin};
+
+pub use core_button::{CoreButton, CoreButtonPlugin};
+pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked};
+pub use core_slider::{
+ CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue,
+ SliderRange, SliderStep, SliderValue, TrackClick,
+};
+
+/// 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, CoreCheckboxPlugin, CoreSliderPlugin));
+ }
+}
diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml
index 2250a35393..ab5a04f2b2 100644
--- a/crates/bevy_dev_tools/Cargo.toml
+++ b/crates/bevy_dev_tools/Cargo.toml
@@ -31,7 +31,7 @@ bevy_state = { path = "../bevy_state", version = "0.16.0-dev" }
# other
serde = { version = "1.0", features = ["derive"], optional = true }
-ron = { version = "0.8.0", optional = true }
+ron = { version = "0.10", optional = true }
tracing = { version = "0.1", default-features = false, features = ["std"] }
[lints]
diff --git a/crates/bevy_dev_tools/src/ci_testing/config.rs b/crates/bevy_dev_tools/src/ci_testing/config.rs
index 6dc601f1cc..a2419dfaa5 100644
--- a/crates/bevy_dev_tools/src/ci_testing/config.rs
+++ b/crates/bevy_dev_tools/src/ci_testing/config.rs
@@ -49,7 +49,7 @@ pub enum CiTestingEvent {
}
/// A custom event that can be configured from a configuration file for CI testing.
-#[derive(Event)]
+#[derive(Event, BufferedEvent)]
pub struct CiTestingCustomEvent(pub String);
#[cfg(test)]
diff --git a/crates/bevy_dev_tools/src/picking_debug.rs b/crates/bevy_dev_tools/src/picking_debug.rs
index 79c1c8fff4..16233cd3dc 100644
--- a/crates/bevy_dev_tools/src/picking_debug.rs
+++ b/crates/bevy_dev_tools/src/picking_debug.rs
@@ -5,9 +5,9 @@ use bevy_color::prelude::*;
use bevy_ecs::prelude::*;
use bevy_picking::backend::HitData;
use bevy_picking::hover::HoverMap;
-use bevy_picking::pointer::{Location, PointerId, PointerPress};
+use bevy_picking::pointer::{Location, PointerId, PointerInput, PointerLocation, PointerPress};
use bevy_picking::prelude::*;
-use bevy_picking::{pointer, PickingSystems};
+use bevy_picking::PickingSystems;
use bevy_reflect::prelude::*;
use bevy_render::prelude::*;
use bevy_text::prelude::*;
@@ -91,11 +91,11 @@ impl Plugin for DebugPickingPlugin {
(
// This leaves room to easily change the log-level associated
// with different events, should that be desired.
- log_event_debug::.run_if(DebugPickingMode::is_noisy),
+ log_event_debug::.run_if(DebugPickingMode::is_noisy),
log_pointer_event_debug::,
log_pointer_event_debug::,
- log_pointer_event_debug::,
- log_pointer_event_debug::,
+ log_pointer_event_debug::,
+ log_pointer_event_debug::,
log_pointer_event_debug::,
log_pointer_event_trace::.run_if(DebugPickingMode::is_noisy),
log_pointer_event_debug::,
@@ -121,7 +121,7 @@ impl Plugin for DebugPickingPlugin {
}
/// Listen for any event and logs it at the debug level
-pub fn log_event_debug(mut events: EventReader) {
+pub fn log_event_debug(mut events: EventReader) {
for event in events.read() {
debug!("{event:?}");
}
@@ -214,7 +214,7 @@ pub fn update_debug_data(
entity_names: Query,
mut pointers: Query<(
&PointerId,
- &pointer::PointerLocation,
+ &PointerLocation,
&PointerPress,
&mut PointerDebug,
)>,
diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml
index cb11b0adcd..6941b71f27 100644
--- a/crates/bevy_ecs/Cargo.toml
+++ b/crates/bevy_ecs/Cargo.toml
@@ -36,7 +36,7 @@ backtrace = ["std"]
## Enables `tracing` integration, allowing spans and other metrics to be reported
## through that framework.
-trace = ["std", "dep:tracing"]
+trace = ["std", "dep:tracing", "bevy_utils/debug"]
## Enables a more detailed set of traces which may be noisy if left on by default.
detailed_trace = ["trace"]
@@ -64,9 +64,9 @@ std = [
"bevy_reflect?/std",
"bevy_tasks/std",
"bevy_utils/parallel",
+ "bevy_utils/std",
"bitflags/std",
"concurrent-queue/std",
- "disqualified/alloc",
"fixedbitset/std",
"indexmap/std",
"serde?/std",
@@ -99,7 +99,6 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-fea
] }
bitflags = { version = "2.3", default-features = false }
-disqualified = { version = "1.0", default-features = false }
fixedbitset = { version = "0.5", default-features = false }
serde = { version = "1", default-features = false, features = [
"alloc",
diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md
index ba283d39b2..b085a79c21 100644
--- a/crates/bevy_ecs/README.md
+++ b/crates/bevy_ecs/README.md
@@ -277,26 +277,24 @@ world.spawn(PlayerBundle {
});
```
-### Events
+### Buffered Events
-Events offer a communication channel between one or more systems. Events can be sent using the system parameter `EventWriter` and received with `EventReader`.
+Buffered events offer a communication channel between one or more systems.
+They can be sent using the `EventWriter` system parameter and received with `EventReader`.
```rust
use bevy_ecs::prelude::*;
-#[derive(Event)]
-struct MyEvent {
- message: String,
+#[derive(Event, BufferedEvent)]
+struct Message(String);
+
+fn writer(mut writer: EventWriter) {
+ writer.write(Message("Hello!".to_string()));
}
-fn writer(mut writer: EventWriter) {
- writer.write(MyEvent {
- message: "hello!".to_string(),
- });
-}
-
-fn reader(mut reader: EventReader) {
- for event in reader.read() {
+fn reader(mut reader: EventReader) {
+ for Message(message) in reader.read() {
+ println!("{}", message);
}
}
```
@@ -309,37 +307,39 @@ Observers are systems that listen for a "trigger" of a specific `Event`:
use bevy_ecs::prelude::*;
#[derive(Event)]
-struct MyEvent {
+struct Speak {
message: String
}
let mut world = World::new();
-world.add_observer(|trigger: Trigger| {
- println!("{}", trigger.event().message);
+world.add_observer(|trigger: On| {
+ println!("{}", trigger.message);
});
world.flush();
-world.trigger(MyEvent {
- message: "hello!".to_string(),
+world.trigger(Speak {
+ message: "Hello!".to_string(),
});
```
-These differ from `EventReader` and `EventWriter` in that they are "reactive". Rather than happening at a specific point in a schedule, they happen _immediately_ whenever a trigger happens. Triggers can trigger other triggers, and they all will be evaluated at the same time!
+These differ from `EventReader` and `EventWriter` in that they are "reactive".
+Rather than happening at a specific point in a schedule, they happen _immediately_ whenever a trigger happens.
+Triggers can trigger other triggers, and they all will be evaluated at the same time!
-Events can also be triggered to target specific entities:
+If the event is an `EntityEvent`, it can also be triggered to target specific entities:
```rust
use bevy_ecs::prelude::*;
-#[derive(Event)]
+#[derive(Event, EntityEvent)]
struct Explode;
let mut world = World::new();
let entity = world.spawn_empty().id();
-world.add_observer(|trigger: Trigger, mut commands: Commands| {
+world.add_observer(|trigger: On, mut commands: Commands| {
println!("Entity {} goes BOOM!", trigger.target());
commands.entity(trigger.target()).despawn();
});
diff --git a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs
index 4076819ee3..79c2158644 100644
--- a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs
+++ b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs
@@ -60,4 +60,4 @@ mod case4 {
pub struct BarTargetOf(Entity);
}
-fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::component::HookContext) {}
+fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::lifecycle::HookContext) {}
diff --git a/crates/bevy_ecs/examples/events.rs b/crates/bevy_ecs/examples/events.rs
index fb01184048..ecdcb31a33 100644
--- a/crates/bevy_ecs/examples/events.rs
+++ b/crates/bevy_ecs/examples/events.rs
@@ -1,4 +1,4 @@
-//! In this example a system sends a custom event with a 50/50 chance during any frame.
+//! In this example a system sends a custom buffered event with a 50/50 chance during any frame.
//! If an event was sent, it will be printed by the console in a receiving system.
#![expect(clippy::print_stdout, reason = "Allowed in examples.")]
@@ -15,7 +15,7 @@ fn main() {
// Create a schedule to store our systems
let mut schedule = Schedule::default();
- // Events need to be updated in every frame in order to clear our buffers.
+ // Buffered events need to be updated every frame in order to clear our buffers.
// This update should happen before we use the events.
// Here, we use system sets to control the ordering.
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
@@ -37,7 +37,7 @@ fn main() {
}
// This is our event that we will send and receive in systems
-#[derive(Event)]
+#[derive(Event, BufferedEvent)]
struct MyEvent {
pub message: String,
pub random_value: f32,
diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs
index 00268cb680..53ba284588 100644
--- a/crates/bevy_ecs/macros/src/component.rs
+++ b/crates/bevy_ecs/macros/src/component.rs
@@ -13,11 +13,28 @@ use syn::{
LitStr, Member, Path, Result, Token, Type, Visibility,
};
-pub const EVENT: &str = "event";
+pub const EVENT: &str = "entity_event";
pub const AUTO_PROPAGATE: &str = "auto_propagate";
pub const TRAVERSAL: &str = "traversal";
pub fn derive_event(input: TokenStream) -> TokenStream {
+ let mut ast = parse_macro_input!(input as DeriveInput);
+ let bevy_ecs_path: Path = crate::bevy_ecs_path();
+
+ ast.generics
+ .make_where_clause()
+ .predicates
+ .push(parse_quote! { Self: Send + Sync + 'static });
+
+ let struct_name = &ast.ident;
+ let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
+
+ TokenStream::from(quote! {
+ impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {}
+ })
+}
+
+pub fn derive_entity_event(input: TokenStream) -> TokenStream {
let mut ast = parse_macro_input!(input as DeriveInput);
let mut auto_propagate = false;
let mut traversal: Type = parse_quote!(());
@@ -49,13 +66,30 @@ pub fn derive_event(input: TokenStream) -> TokenStream {
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
TokenStream::from(quote! {
- impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {
+ impl #impl_generics #bevy_ecs_path::event::EntityEvent for #struct_name #type_generics #where_clause {
type Traversal = #traversal;
const AUTO_PROPAGATE: bool = #auto_propagate;
}
})
}
+pub fn derive_buffered_event(input: TokenStream) -> TokenStream {
+ let mut ast = parse_macro_input!(input as DeriveInput);
+ let bevy_ecs_path: Path = crate::bevy_ecs_path();
+
+ ast.generics
+ .make_where_clause()
+ .predicates
+ .push(parse_quote! { Self: Send + Sync + 'static });
+
+ let struct_name = &ast.ident;
+ let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
+
+ TokenStream::from(quote! {
+ impl #impl_generics #bevy_ecs_path::event::BufferedEvent for #struct_name #type_generics #where_clause {}
+ })
+}
+
pub fn derive_resource(input: TokenStream) -> TokenStream {
let mut ast = parse_macro_input!(input as DeriveInput);
let bevy_ecs_path: Path = crate::bevy_ecs_path();
@@ -434,7 +468,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 +692,7 @@ fn hook_register_function_call(
) -> Option {
function.map(|meta| {
quote! {
- fn #hook() -> ::core::option::Option<#bevy_ecs_path::component::ComponentHook> {
+ fn #hook() -> ::core::option::Option<#bevy_ecs_path::lifecycle::ComponentHook> {
::core::option::Option::Some(#meta)
}
}
diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs
index 114aff642b..7750f97259 100644
--- a/crates/bevy_ecs/macros/src/lib.rs
+++ b/crates/bevy_ecs/macros/src/lib.rs
@@ -1,4 +1,5 @@
-#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
+//! Macros for deriving ECS traits.
+
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
extern crate proc_macro;
@@ -15,7 +16,7 @@ use crate::{
use bevy_macro_utils::{derive_label, ensure_no_collision, get_struct_fields, BevyManifest};
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
-use quote::{format_ident, quote};
+use quote::{format_ident, quote, ToTokens};
use syn::{
parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma,
ConstParam, Data, DataStruct, DeriveInput, GenericParam, Index, TypeParam,
@@ -28,12 +29,48 @@ 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))]
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(),
@@ -42,6 +79,8 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
let mut field_kind = Vec::with_capacity(named_fields.len());
for field in named_fields {
+ let mut kind = BundleFieldKind::Component;
+
for attr in field
.attrs
.iter()
@@ -49,7 +88,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
{
if let Err(error) = attr.parse_nested_meta(|meta| {
if meta.path.is_ident(BUNDLE_ATTRIBUTE_IGNORE_NAME) {
- field_kind.push(BundleFieldKind::Ignore);
+ kind = BundleFieldKind::Ignore;
Ok(())
} else {
Err(meta.error(format!(
@@ -61,7 +100,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
}
}
- field_kind.push(BundleFieldKind::Component);
+ field_kind.push(kind);
}
let field = named_fields
@@ -74,61 +113,33 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
.map(|field| &field.ty)
.collect::>();
- let mut field_component_ids = Vec::new();
- let mut field_get_component_ids = Vec::new();
- let mut field_get_components = Vec::new();
- let mut field_from_components = Vec::new();
- let mut field_required_components = Vec::new();
+ let mut active_field_types = Vec::new();
+ let mut active_field_tokens = Vec::new();
+ let mut inactive_field_tokens = Vec::new();
for (((i, field_type), field_kind), field) in field_type
.iter()
.enumerate()
.zip(field_kind.iter())
.zip(field.iter())
{
+ let field_tokens = match field {
+ Some(field) => field.to_token_stream(),
+ None => Index::from(i).to_token_stream(),
+ };
match field_kind {
BundleFieldKind::Component => {
- field_component_ids.push(quote! {
- <#field_type as #ecs_path::bundle::Bundle>::component_ids(components, &mut *ids);
- });
- field_required_components.push(quote! {
- <#field_type as #ecs_path::bundle::Bundle>::register_required_components(components, required_components);
- });
- field_get_component_ids.push(quote! {
- <#field_type as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids);
- });
- match field {
- Some(field) => {
- field_get_components.push(quote! {
- self.#field.get_components(&mut *func);
- });
- field_from_components.push(quote! {
- #field: <#field_type as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func),
- });
- }
- None => {
- let index = Index::from(i);
- field_get_components.push(quote! {
- self.#index.get_components(&mut *func);
- });
- field_from_components.push(quote! {
- #index: <#field_type as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func),
- });
- }
- }
+ active_field_types.push(field_type);
+ active_field_tokens.push(field_tokens);
}
- BundleFieldKind::Ignore => {
- field_from_components.push(quote! {
- #field: ::core::default::Default::default(),
- });
- }
+ BundleFieldKind::Ignore => inactive_field_tokens.push(field_tokens),
}
}
let generics = ast.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let struct_name = &ast.ident;
- TokenStream::from(quote! {
+ let bundle_impl = quote! {
// 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
@@ -138,40 +149,27 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
fn component_ids(
components: &mut #ecs_path::component::ComponentsRegistrator,
ids: &mut impl FnMut(#ecs_path::component::ComponentId)
- ){
- #(#field_component_ids)*
+ ) {
+ #(<#active_field_types as #ecs_path::bundle::Bundle>::component_ids(components, ids);)*
}
fn get_component_ids(
components: ecs_path::component::Components,
ids: &mut impl FnMut(Option<#ecs_path::component::ComponentId>)
- ){
- #(#field_get_component_ids)*
+ ) {
+ #(<#active_field_types as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids);)*
}
fn register_required_components(
components: &mut #ecs_path::component::ComponentsRegistrator,
required_components: &mut #ecs_path::component::RequiredComponents
- ){
- #(#field_required_components)*
- }
- }
-
- // 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)*
- }
+ ) {
+ #(<#active_field_types as #ecs_path::bundle::Bundle>::register_required_components(components, required_components);)*
}
}
+ };
+ let dynamic_bundle_impl = quote! {
#[allow(deprecated)]
impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause {
type Effect = ();
@@ -181,12 +179,40 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
self,
func: &mut impl FnMut(#ecs_path::component::StorageType, #ecs_path::ptr::OwningPtr<'_>)
) {
- #(#field_get_components)*
+ #(<#active_field_types as #ecs_path::bundle::DynamicBundle>::get_components(self.#active_field_tokens, &mut *func);)*
}
}
+ };
+
+ let from_components_impl = 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 {
+ #(#active_field_tokens: <#active_field_types as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func),)*
+ #(#inactive_field_tokens: ::core::default::Default::default(),)*
+ }
+ }
+ }
+ });
+
+ let attribute_errors = &errors;
+
+ TokenStream::from(quote! {
+ #(#attribute_errors)*
+ #bundle_impl
+ #from_components_impl
+ #dynamic_bundle_impl
})
}
+/// Implement the `MapEntities` trait.
#[proc_macro_derive(MapEntities, attributes(entities))]
pub fn derive_map_entities(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
@@ -394,10 +420,10 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream {
> #path::system::SystemParamBuilder<#generic_struct> for #builder_name<#(#builder_type_parameters,)*>
#where_clause
{
- fn build(self, world: &mut #path::world::World, meta: &mut #path::system::SystemMeta) -> <#generic_struct as #path::system::SystemParam>::State {
+ fn build(self, world: &mut #path::world::World) -> <#generic_struct as #path::system::SystemParam>::State {
let #builder_name { #(#fields: #field_locals,)* } = self;
#state_struct_name {
- state: #path::system::SystemParamBuilder::build((#(#tuple_patterns,)*), world, meta)
+ state: #path::system::SystemParamBuilder::build((#(#tuple_patterns,)*), world)
}
}
}
@@ -426,12 +452,16 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream {
type State = #state_struct_name<#punctuated_generic_idents>;
type Item<'w, 's> = #struct_name #ty_generics;
- fn init_state(world: &mut #path::world::World, system_meta: &mut #path::system::SystemMeta) -> Self::State {
+ fn init_state(world: &mut #path::world::World) -> Self::State {
#state_struct_name {
- state: <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::init_state(world, system_meta),
+ state: <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::init_state(world),
}
}
+ fn init_access(state: &Self::State, system_meta: &mut #path::system::SystemMeta, component_access_set: &mut #path::query::FilteredAccessSet<#path::component::ComponentId>, world: &mut #path::world::World) {
+ <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::init_access(&state.state, system_meta, component_access_set, world);
+ }
+
fn apply(state: &mut Self::State, system_meta: path::system::SystemMeta, world: &mut #path::world::World) {
<#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::apply(&mut state.state, system_meta, world);
}
@@ -522,16 +552,31 @@ pub(crate) fn bevy_ecs_path() -> syn::Path {
BevyManifest::shared().get_path("bevy_ecs")
}
-#[proc_macro_derive(Event, attributes(event))]
+/// Implement the `Event` trait.
+#[proc_macro_derive(Event)]
pub fn derive_event(input: TokenStream) -> TokenStream {
component::derive_event(input)
}
+/// Implement the `EntityEvent` trait.
+#[proc_macro_derive(EntityEvent, attributes(entity_event))]
+pub fn derive_entity_event(input: TokenStream) -> TokenStream {
+ component::derive_entity_event(input)
+}
+
+/// Implement the `BufferedEvent` trait.
+#[proc_macro_derive(BufferedEvent)]
+pub fn derive_buffered_event(input: TokenStream) -> TokenStream {
+ component::derive_buffered_event(input)
+}
+
+/// Implement the `Resource` trait.
#[proc_macro_derive(Resource)]
pub fn derive_resource(input: TokenStream) -> TokenStream {
component::derive_resource(input)
}
+/// Implement the `Component` trait.
#[proc_macro_derive(
Component,
attributes(component, require, relationship, relationship_target, entities)
@@ -540,6 +585,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
component::derive_component(input)
}
+/// Implement the `FromWorld` trait.
#[proc_macro_derive(FromWorld, attributes(from_world))]
pub fn derive_from_world(input: TokenStream) -> TokenStream {
let bevy_ecs_path = bevy_ecs_path();
diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs
index 4e4529e631..12d9c2bf1c 100644
--- a/crates/bevy_ecs/macros/src/query_data.rs
+++ b/crates/bevy_ecs/macros/src/query_data.rs
@@ -74,12 +74,23 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream {
let user_generics = ast.generics.clone();
let (user_impl_generics, user_ty_generics, user_where_clauses) = user_generics.split_for_impl();
let user_generics_with_world = {
- let mut generics = ast.generics;
+ let mut generics = ast.generics.clone();
generics.params.insert(0, parse_quote!('__w));
generics
};
let (user_impl_generics_with_world, user_ty_generics_with_world, user_where_clauses_with_world) =
user_generics_with_world.split_for_impl();
+ let user_generics_with_world_and_state = {
+ let mut generics = ast.generics;
+ generics.params.insert(0, parse_quote!('__w));
+ generics.params.insert(1, parse_quote!('__s));
+ generics
+ };
+ let (
+ user_impl_generics_with_world_and_state,
+ user_ty_generics_with_world_and_state,
+ user_where_clauses_with_world_and_state,
+ ) = user_generics_with_world_and_state.split_for_impl();
let struct_name = ast.ident;
let read_only_struct_name = if attributes.is_mutable {
@@ -164,13 +175,13 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream {
&visibility,
&item_struct_name,
&field_types,
- &user_impl_generics_with_world,
+ &user_impl_generics_with_world_and_state,
&field_attrs,
&field_visibilities,
&field_idents,
&user_ty_generics,
- &user_ty_generics_with_world,
- user_where_clauses_with_world,
+ &user_ty_generics_with_world_and_state,
+ user_where_clauses_with_world_and_state,
);
let mutable_world_query_impl = world_query_impl(
&path,
@@ -199,13 +210,13 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream {
&visibility,
&read_only_item_struct_name,
&read_only_field_types,
- &user_impl_generics_with_world,
+ &user_impl_generics_with_world_and_state,
&field_attrs,
&field_visibilities,
&field_idents,
&user_ty_generics,
- &user_ty_generics_with_world,
- user_where_clauses_with_world,
+ &user_ty_generics_with_world_and_state,
+ user_where_clauses_with_world_and_state,
);
let readonly_world_query_impl = world_query_impl(
&path,
@@ -256,11 +267,11 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream {
for #read_only_struct_name #user_ty_generics #user_where_clauses {
const IS_READ_ONLY: bool = true;
type ReadOnly = #read_only_struct_name #user_ty_generics;
- type Item<'__w> = #read_only_item_struct_name #user_ty_generics_with_world;
+ type Item<'__w, '__s> = #read_only_item_struct_name #user_ty_generics_with_world_and_state;
- fn shrink<'__wlong: '__wshort, '__wshort>(
- item: Self::Item<'__wlong>
- ) -> Self::Item<'__wshort> {
+ fn shrink<'__wlong: '__wshort, '__wshort, '__s>(
+ item: Self::Item<'__wlong, '__s>
+ ) -> Self::Item<'__wshort, '__s> {
#read_only_item_struct_name {
#(
#field_idents: <#read_only_field_types>::shrink(item.#field_idents),
@@ -278,13 +289,26 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream {
/// SAFETY: we call `fetch` for each member that implements `Fetch`.
#[inline(always)]
- unsafe fn fetch<'__w>(
+ unsafe fn fetch<'__w, '__s>(
+ _state: &'__s Self::State,
_fetch: &mut ::Fetch<'__w>,
_entity: #path::entity::Entity,
_table_row: #path::storage::TableRow,
- ) -> Self::Item<'__w> {
+ ) -> Self::Item<'__w, '__s> {
Self::Item {
- #(#field_idents: <#read_only_field_types>::fetch(&mut _fetch.#named_field_idents, _entity, _table_row),)*
+ #(#field_idents: <#read_only_field_types>::fetch(&_state.#named_field_idents, &mut _fetch.#named_field_idents, _entity, _table_row),)*
+ }
+ }
+ }
+
+ impl #user_impl_generics #path::query::ReleaseStateQueryData
+ for #read_only_struct_name #user_ty_generics #user_where_clauses
+ // Make these HRTBs with an unused lifetime parameter to allow trivial constraints
+ // See https://github.com/rust-lang/rust/issues/48214
+ where #(for<'__a> #field_types: #path::query::QueryData,)* {
+ fn release_state<'__w>(_item: Self::Item<'__w, '_>) -> Self::Item<'__w, 'static> {
+ Self::Item {
+ #(#field_idents: <#read_only_field_types>::release_state(_item.#field_idents),)*
}
}
}
@@ -301,11 +325,11 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream {
for #struct_name #user_ty_generics #user_where_clauses {
const IS_READ_ONLY: bool = #is_read_only;
type ReadOnly = #read_only_struct_name #user_ty_generics;
- type Item<'__w> = #item_struct_name #user_ty_generics_with_world;
+ type Item<'__w, '__s> = #item_struct_name #user_ty_generics_with_world_and_state;
- fn shrink<'__wlong: '__wshort, '__wshort>(
- item: Self::Item<'__wlong>
- ) -> Self::Item<'__wshort> {
+ fn shrink<'__wlong: '__wshort, '__wshort, '__s>(
+ item: Self::Item<'__wlong, '__s>
+ ) -> Self::Item<'__wshort, '__s> {
#item_struct_name {
#(
#field_idents: <#field_types>::shrink(item.#field_idents),
@@ -323,13 +347,26 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream {
/// SAFETY: we call `fetch` for each member that implements `Fetch`.
#[inline(always)]
- unsafe fn fetch<'__w>(
+ unsafe fn fetch<'__w, '__s>(
+ _state: &'__s Self::State,
_fetch: &mut ::Fetch<'__w>,
_entity: #path::entity::Entity,
_table_row: #path::storage::TableRow,
- ) -> Self::Item<'__w> {
+ ) -> Self::Item<'__w, '__s> {
Self::Item {
- #(#field_idents: <#field_types>::fetch(&mut _fetch.#named_field_idents, _entity, _table_row),)*
+ #(#field_idents: <#field_types>::fetch(&_state.#named_field_idents, &mut _fetch.#named_field_idents, _entity, _table_row),)*
+ }
+ }
+ }
+
+ impl #user_impl_generics #path::query::ReleaseStateQueryData
+ for #struct_name #user_ty_generics #user_where_clauses
+ // Make these HRTBs with an unused lifetime parameter to allow trivial constraints
+ // See https://github.com/rust-lang/rust/issues/48214
+ where #(for<'__a> #field_types: #path::query::ReleaseStateQueryData,)* {
+ fn release_state<'__w>(_item: Self::Item<'__w, '_>) -> Self::Item<'__w, 'static> {
+ Self::Item {
+ #(#field_idents: <#field_types>::release_state(_item.#field_idents),)*
}
}
}
diff --git a/crates/bevy_ecs/macros/src/query_filter.rs b/crates/bevy_ecs/macros/src/query_filter.rs
index c7ddb9cc83..5ae2d2325f 100644
--- a/crates/bevy_ecs/macros/src/query_filter.rs
+++ b/crates/bevy_ecs/macros/src/query_filter.rs
@@ -102,11 +102,12 @@ pub fn derive_query_filter_impl(input: TokenStream) -> TokenStream {
#[allow(unused_variables)]
#[inline(always)]
unsafe fn filter_fetch<'__w>(
+ _state: &Self::State,
_fetch: &mut ::Fetch<'__w>,
_entity: #path::entity::Entity,
_table_row: #path::storage::TableRow,
) -> bool {
- true #(&& <#field_types>::filter_fetch(&mut _fetch.#named_field_idents, _entity, _table_row))*
+ true #(&& <#field_types>::filter_fetch(&_state.#named_field_idents, &mut _fetch.#named_field_idents, _entity, _table_row))*
}
}
};
diff --git a/crates/bevy_ecs/macros/src/world_query.rs b/crates/bevy_ecs/macros/src/world_query.rs
index 5c4c0bff01..5a7d164b80 100644
--- a/crates/bevy_ecs/macros/src/world_query.rs
+++ b/crates/bevy_ecs/macros/src/world_query.rs
@@ -10,13 +10,13 @@ pub(crate) fn item_struct(
visibility: &Visibility,
item_struct_name: &Ident,
field_types: &Vec,
- user_impl_generics_with_world: &ImplGenerics,
+ user_impl_generics_with_world_and_state: &ImplGenerics,
field_attrs: &Vec>,
field_visibilities: &Vec,
field_idents: &Vec,
user_ty_generics: &TypeGenerics,
- user_ty_generics_with_world: &TypeGenerics,
- user_where_clauses_with_world: Option<&WhereClause>,
+ user_ty_generics_with_world_and_state: &TypeGenerics,
+ user_where_clauses_with_world_and_state: Option<&WhereClause>,
) -> proc_macro2::TokenStream {
let item_attrs = quote! {
#[doc = concat!(
@@ -33,20 +33,20 @@ pub(crate) fn item_struct(
Fields::Named(_) => quote! {
#derive_macro_call
#item_attrs
- #visibility struct #item_struct_name #user_impl_generics_with_world #user_where_clauses_with_world {
- #(#(#field_attrs)* #field_visibilities #field_idents: <#field_types as #path::query::QueryData>::Item<'__w>,)*
+ #visibility struct #item_struct_name #user_impl_generics_with_world_and_state #user_where_clauses_with_world_and_state {
+ #(#(#field_attrs)* #field_visibilities #field_idents: <#field_types as #path::query::QueryData>::Item<'__w, '__s>,)*
}
},
Fields::Unnamed(_) => quote! {
#derive_macro_call
#item_attrs
- #visibility struct #item_struct_name #user_impl_generics_with_world #user_where_clauses_with_world(
- #( #field_visibilities <#field_types as #path::query::QueryData>::Item<'__w>, )*
+ #visibility struct #item_struct_name #user_impl_generics_with_world_and_state #user_where_clauses_with_world_and_state(
+ #( #field_visibilities <#field_types as #path::query::QueryData>::Item<'__w, '__s>, )*
);
},
Fields::Unit => quote! {
#item_attrs
- #visibility type #item_struct_name #user_ty_generics_with_world = #struct_name #user_ty_generics;
+ #visibility type #item_struct_name #user_ty_generics_with_world_and_state = #struct_name #user_ty_generics;
},
}
}
@@ -79,7 +79,7 @@ pub(crate) fn world_query_impl(
#[automatically_derived]
#visibility struct #fetch_struct_name #user_impl_generics_with_world #user_where_clauses_with_world {
#(#named_field_idents: <#field_types as #path::query::WorldQuery>::Fetch<'__w>,)*
- #marker_name: &'__w (),
+ #marker_name: &'__w(),
}
impl #user_impl_generics_with_world Clone for #fetch_struct_name #user_ty_generics_with_world
@@ -110,9 +110,9 @@ pub(crate) fn world_query_impl(
}
}
- unsafe fn init_fetch<'__w>(
+ unsafe fn init_fetch<'__w, '__s>(
_world: #path::world::unsafe_world_cell::UnsafeWorldCell<'__w>,
- state: &Self::State,
+ state: &'__s Self::State,
_last_run: #path::component::Tick,
_this_run: #path::component::Tick,
) -> ::Fetch<'__w> {
@@ -133,9 +133,9 @@ pub(crate) fn world_query_impl(
/// SAFETY: we call `set_archetype` for each member that implements `Fetch`
#[inline]
- unsafe fn set_archetype<'__w>(
+ unsafe fn set_archetype<'__w, '__s>(
_fetch: &mut ::Fetch<'__w>,
- _state: &Self::State,
+ _state: &'__s Self::State,
_archetype: &'__w #path::archetype::Archetype,
_table: &'__w #path::storage::Table
) {
@@ -144,9 +144,9 @@ pub(crate) fn world_query_impl(
/// SAFETY: we call `set_table` for each member that implements `Fetch`
#[inline]
- unsafe fn set_table<'__w>(
+ unsafe fn set_table<'__w, '__s>(
_fetch: &mut ::Fetch<'__w>,
- _state: &Self::State,
+ _state: &'__s Self::State,
_table: &'__w #path::storage::Table
) {
#(<#field_types>::set_table(&mut _fetch.#named_field_idents, &_state.#named_field_idents, _table);)*
diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs
index 4e5b28dde8..34a2a4c813 100644
--- a/crates/bevy_ecs/src/archetype.rs
+++ b/crates/bevy_ecs/src/archetype.rs
@@ -691,41 +691,41 @@ impl Archetype {
self.flags().contains(ArchetypeFlags::ON_DESPAWN_HOOK)
}
- /// Returns true if any of the components in this archetype have at least one [`OnAdd`] observer
+ /// Returns true if any of the components in this archetype have at least one [`Add`] observer
///
- /// [`OnAdd`]: crate::world::OnAdd
+ /// [`Add`]: crate::lifecycle::Add
#[inline]
pub fn has_add_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_ADD_OBSERVER)
}
- /// Returns true if any of the components in this archetype have at least one [`OnInsert`] observer
+ /// Returns true if any of the components in this archetype have at least one [`Insert`] observer
///
- /// [`OnInsert`]: crate::world::OnInsert
+ /// [`Insert`]: crate::lifecycle::Insert
#[inline]
pub fn has_insert_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER)
}
- /// Returns true if any of the components in this archetype have at least one [`OnReplace`] observer
+ /// Returns true if any of the components in this archetype have at least one [`Replace`] observer
///
- /// [`OnReplace`]: crate::world::OnReplace
+ /// [`Replace`]: crate::lifecycle::Replace
#[inline]
pub fn has_replace_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_REPLACE_OBSERVER)
}
- /// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer
+ /// Returns true if any of the components in this archetype have at least one [`Remove`] observer
///
- /// [`OnRemove`]: crate::world::OnRemove
+ /// [`Remove`]: crate::lifecycle::Remove
#[inline]
pub fn has_remove_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER)
}
- /// Returns true if any of the components in this archetype have at least one [`OnDespawn`] observer
+ /// Returns true if any of the components in this archetype have at least one [`Despawn`] observer
///
- /// [`OnDespawn`]: crate::world::OnDespawn
+ /// [`Despawn`]: crate::lifecycle::Despawn
#[inline]
pub fn has_despawn_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_DESPAWN_OBSERVER)
diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs
index f0641ff80c..8efdc60ad9 100644
--- a/crates/bevy_ecs/src/bundle.rs
+++ b/crates/bevy_ecs/src/bundle.rs
@@ -2,6 +2,57 @@
//!
//! This module contains the [`Bundle`] trait and some other helper types.
+/// Derive the [`Bundle`] trait
+///
+/// You can apply this derive macro to structs that are
+/// composed of [`Component`]s or
+/// other [`Bundle`]s.
+///
+/// ## Attributes
+///
+/// Sometimes parts of the Bundle should not be inserted.
+/// Those can be marked with `#[bundle(ignore)]`, and they will be skipped.
+/// In that case, the field needs to implement [`Default`] unless you also ignore
+/// the [`BundleFromComponents`] implementation.
+///
+/// ```rust
+/// # use bevy_ecs::prelude::{Component, Bundle};
+/// # #[derive(Component)]
+/// # struct Hitpoint;
+/// #
+/// #[derive(Bundle)]
+/// struct HitpointMarker {
+/// hitpoints: Hitpoint,
+///
+/// #[bundle(ignore)]
+/// creator: Option
+/// }
+/// ```
+///
+/// Some fields may be bundles that do not implement
+/// [`BundleFromComponents`]. This happens for bundles that cannot be extracted.
+/// For example with [`SpawnRelatedBundle`](bevy_ecs::spawn::SpawnRelatedBundle), see below for an
+/// example usage.
+/// In those cases you can either ignore it as above,
+/// or you can opt out the whole Struct by marking it as ignored with
+/// `#[bundle(ignore_from_components)]`.
+///
+/// ```rust
+/// # use bevy_ecs::prelude::{Component, Bundle, ChildOf, Spawn};
+/// # #[derive(Component)]
+/// # struct Hitpoint;
+/// # #[derive(Component)]
+/// # struct Marker;
+/// #
+/// use bevy_ecs::spawn::SpawnRelatedBundle;
+///
+/// #[derive(Bundle)]
+/// #[bundle(ignore_from_components)]
+/// struct HitpointMarker {
+/// hitpoints: Hitpoint,
+/// related_spawner: SpawnRelatedBundle>,
+/// }
+/// ```
pub use bevy_ecs_macros::Bundle;
use crate::{
@@ -15,15 +66,13 @@ use crate::{
RequiredComponents, StorageType, Tick,
},
entity::{Entities, Entity, EntityLocation},
+ lifecycle::{ADD, INSERT, REMOVE, 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};
@@ -501,10 +550,9 @@ impl BundleInfo {
// SAFETY: the caller ensures component_id is valid.
unsafe { components.get_info_unchecked(id).name() }
})
- .collect::>()
- .join(", ");
+ .collect::>();
- panic!("Bundle {bundle_type_name} has duplicate components: {names}");
+ panic!("Bundle {bundle_type_name} has duplicate components: {names:?}");
}
// handle explicit components
@@ -1142,8 +1190,8 @@ impl<'w> BundleInserter<'w> {
if insert_mode == InsertMode::Replace {
if archetype.has_replace_observer() {
deferred_world.trigger_observers(
- ON_REPLACE,
- entity,
+ REPLACE,
+ Some(entity),
archetype_after_insert.iter_existing(),
caller,
);
@@ -1327,8 +1375,8 @@ impl<'w> BundleInserter<'w> {
);
if new_archetype.has_add_observer() {
deferred_world.trigger_observers(
- ON_ADD,
- entity,
+ ADD,
+ Some(entity),
archetype_after_insert.iter_added(),
caller,
);
@@ -1345,8 +1393,8 @@ impl<'w> BundleInserter<'w> {
);
if new_archetype.has_insert_observer() {
deferred_world.trigger_observers(
- ON_INSERT,
- entity,
+ INSERT,
+ Some(entity),
archetype_after_insert.iter_inserted(),
caller,
);
@@ -1364,8 +1412,8 @@ impl<'w> BundleInserter<'w> {
);
if new_archetype.has_insert_observer() {
deferred_world.trigger_observers(
- ON_INSERT,
- entity,
+ INSERT,
+ Some(entity),
archetype_after_insert.iter_added(),
caller,
);
@@ -1518,8 +1566,8 @@ impl<'w> BundleRemover<'w> {
};
if self.old_archetype.as_ref().has_replace_observer() {
deferred_world.trigger_observers(
- ON_REPLACE,
- entity,
+ REPLACE,
+ Some(entity),
bundle_components_in_archetype(),
caller,
);
@@ -1533,8 +1581,8 @@ impl<'w> BundleRemover<'w> {
);
if self.old_archetype.as_ref().has_remove_observer() {
deferred_world.trigger_observers(
- ON_REMOVE,
- entity,
+ REMOVE,
+ Some(entity),
bundle_components_in_archetype(),
caller,
);
@@ -1784,8 +1832,8 @@ impl<'w> BundleSpawner<'w> {
);
if archetype.has_add_observer() {
deferred_world.trigger_observers(
- ON_ADD,
- entity,
+ ADD,
+ Some(entity),
bundle_info.iter_contributed_components(),
caller,
);
@@ -1799,8 +1847,8 @@ impl<'w> BundleSpawner<'w> {
);
if archetype.has_insert_observer() {
deferred_world.trigger_observers(
- ON_INSERT,
- entity,
+ INSERT,
+ Some(entity),
bundle_info.iter_contributed_components(),
caller,
);
@@ -2072,7 +2120,7 @@ fn sorted_remove(source: &mut Vec, remove: &[T]) {
#[cfg(test)]
mod tests {
use crate::{
- archetype::ArchetypeCreated, component::HookContext, prelude::*, world::DeferredWorld,
+ archetype::ArchetypeCreated, lifecycle::HookContext, prelude::*, world::DeferredWorld,
};
use alloc::vec;
@@ -2122,6 +2170,26 @@ mod tests {
}
}
+ #[derive(Bundle)]
+ #[bundle(ignore_from_components)]
+ struct BundleNoExtract {
+ b: B,
+ no_from_comp: crate::spawn::SpawnRelatedBundle>,
+ }
+
+ #[test]
+ fn can_spawn_bundle_without_extract() {
+ let mut world = World::new();
+ let id = world
+ .spawn(BundleNoExtract {
+ b: B,
+ no_from_comp: Children::spawn(Spawn(C)),
+ })
+ .id();
+
+ assert!(world.entity(id).get::().is_some());
+ }
+
#[test]
fn component_hook_order_spawn_despawn() {
let mut world = World::new();
@@ -2317,7 +2385,7 @@ mod tests {
#[derive(Resource, Default)]
struct Count(u32);
world.init_resource::();
- world.add_observer(|_t: Trigger, mut count: ResMut| {
+ world.add_observer(|_t: On, mut count: ResMut| {
count.0 += 1;
});
@@ -2329,4 +2397,13 @@ mod tests {
assert_eq!(world.resource::().0, 3);
}
+
+ #[derive(Bundle)]
+ #[expect(unused, reason = "tests the output of the derive macro is valid")]
+ struct Ignore {
+ #[bundle(ignore)]
+ foo: i32,
+ #[bundle(ignore)]
+ bar: i32,
+ }
}
diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs
index 85219d44ca..006b738caf 100644
--- a/crates/bevy_ecs/src/change_detection.rs
+++ b/crates/bevy_ecs/src/change_detection.rs
@@ -230,7 +230,7 @@ pub trait DetectChangesMut: DetectChanges {
/// #[derive(Resource, PartialEq, Eq)]
/// pub struct Score(u32);
///
- /// #[derive(Event, PartialEq, Eq)]
+ /// #[derive(Event, BufferedEvent, PartialEq, Eq)]
/// pub struct ScoreChanged {
/// current: u32,
/// previous: u32,
@@ -898,63 +898,39 @@ impl_debug!(Ref<'w, T>,);
/// Unique mutable borrow of an entity's component or of a resource.
///
-/// This can be used in queries to opt into change detection on both their mutable and immutable forms, as opposed to
-/// `&mut T`, which only provides access to change detection while in its mutable form:
+/// This can be used in queries to access change detection from immutable query methods, as opposed
+/// to `&mut T` which only provides access to change detection from mutable query methods.
///
/// ```rust
/// # use bevy_ecs::prelude::*;
/// # use bevy_ecs::query::QueryData;
/// #
-/// #[derive(Component, Clone)]
+/// #[derive(Component, Clone, Debug)]
/// struct Name(String);
///
-/// #[derive(Component, Clone, Copy)]
+/// #[derive(Component, Clone, Copy, Debug)]
/// struct Health(f32);
///
-/// #[derive(Component, Clone, Copy)]
-/// struct Position {
-/// x: f32,
-/// y: f32,
-/// };
+/// fn my_system(mut query: Query<(Mut, &mut Health)>) {
+/// // Mutable access provides change detection information for both parameters:
+/// // - `name` has type `Mut`
+/// // - `health` has type `Mut`
+/// for (name, health) in query.iter_mut() {
+/// println!("Name: {:?} (last changed {:?})", name, name.last_changed());
+/// println!("Health: {:?} (last changed: {:?})", health, health.last_changed());
+/// # println!("{}{}", name.0, health.0); // Silence dead_code warning
+/// }
///
-/// #[derive(Component, Clone, Copy)]
-/// struct Player {
-/// id: usize,
-/// };
-///
-/// #[derive(QueryData)]
-/// #[query_data(mutable)]
-/// struct PlayerQuery {
-/// id: &'static Player,
-///
-/// // Reacting to `PlayerName` changes is expensive, so we need to enable change detection when reading it.
-/// name: Mut<'static, Name>,
-///
-/// health: &'static mut Health,
-/// position: &'static mut Position,
-/// }
-///
-/// fn update_player_avatars(players_query: Query) {
-/// // The item returned by the iterator is of type `PlayerQueryReadOnlyItem`.
-/// for player in players_query.iter() {
-/// if player.name.is_changed() {
-/// // Update the player's name. This clones a String, and so is more expensive.
-/// update_player_name(player.id, player.name.clone());
-/// }
-///
-/// // Update the health bar.
-/// update_player_health(player.id, *player.health);
-///
-/// // Update the player's position.
-/// update_player_position(player.id, *player.position);
+/// // Immutable access only provides change detection for `Name`:
+/// // - `name` has type `Ref`
+/// // - `health` has type `&Health`
+/// for (name, health) in query.iter() {
+/// println!("Name: {:?} (last changed {:?})", name, name.last_changed());
+/// println!("Health: {:?}", health);
/// }
/// }
///
-/// # bevy_ecs::system::assert_is_system(update_player_avatars);
-///
-/// # fn update_player_name(player: &Player, new_name: Name) {}
-/// # fn update_player_health(player: &Player, new_health: Health) {}
-/// # fn update_player_position(player: &Player, new_position: Position) {}
+/// # bevy_ecs::system::assert_is_system(my_system);
/// ```
pub struct Mut<'w, T: ?Sized> {
pub(crate) value: &'w mut T,
diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs
index d083901ccc..cfcde29ab2 100644
--- a/crates/bevy_ecs/src/component.rs
+++ b/crates/bevy_ecs/src/component.rs
@@ -5,16 +5,17 @@ use crate::{
bundle::BundleInfo,
change_detection::{MaybeLocation, MAX_CHANGE_AGE},
entity::{ComponentCloneCtx, Entity, EntityMapper, SourceComponent},
+ lifecycle::{ComponentHook, ComponentHooks},
query::DebugCheckedUnwrap,
- relationship::RelationshipHookMode,
resource::Resource,
storage::{SparseSetIndex, SparseSets, Table, TableRow},
system::{Local, SystemParam},
- world::{DeferredWorld, FromWorld, World},
+ world::{FromWorld, World},
};
use alloc::boxed::Box;
use alloc::{borrow::Cow, format, vec::Vec};
pub use bevy_ecs_macros::Component;
+use bevy_ecs_macros::Event;
use bevy_platform::sync::Arc;
use bevy_platform::{
collections::{HashMap, HashSet},
@@ -23,7 +24,7 @@ use bevy_platform::{
use bevy_ptr::{OwningPtr, UnsafeCellDeref};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
-use bevy_utils::TypeIdMap;
+use bevy_utils::{prelude::DebugName, TypeIdMap};
use core::{
alloc::Layout,
any::{Any, TypeId},
@@ -33,7 +34,6 @@ use core::{
mem::needs_drop,
ops::{Deref, DerefMut},
};
-use disqualified::ShortName;
use smallvec::SmallVec;
use thiserror::Error;
@@ -375,7 +375,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 +405,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)]
@@ -595,7 +597,7 @@ mod private {
/// `&mut ...`, created while inserted onto an entity.
/// In all other ways, they are identical to mutable components.
/// This restriction allows hooks to observe all changes made to an immutable
-/// component, effectively turning the `OnInsert` and `OnReplace` hooks into a
+/// component, effectively turning the `Insert` and `Replace` hooks into a
/// `OnMutate` hook.
/// This is not practical for mutable components, as the runtime cost of invoking
/// a hook for every exclusive reference created would be far too high.
@@ -656,244 +658,6 @@ pub enum StorageType {
SparseSet,
}
-/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove`.
-pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, HookContext);
-
-/// Context provided to a [`ComponentHook`].
-#[derive(Clone, Copy, Debug)]
-pub struct HookContext {
- /// The [`Entity`] this hook was invoked for.
- pub entity: Entity,
- /// The [`ComponentId`] this hook was invoked for.
- pub component_id: ComponentId,
- /// The caller location is `Some` if the `track_caller` feature is enabled.
- pub caller: MaybeLocation,
- /// Configures how relationship hooks will run
- pub relationship_hook_mode: RelationshipHookMode,
-}
-
-/// [`World`]-mutating functions that run as part of lifecycle events of a [`Component`].
-///
-/// Hooks are functions that run when a component is added, overwritten, or removed from an entity.
-/// These are intended to be used for structural side effects that need to happen when a component is added or removed,
-/// and are not intended for general-purpose logic.
-///
-/// For example, you might use a hook to update a cached index when a component is added,
-/// to clean up resources when a component is removed,
-/// or to keep hierarchical data structures across entities in sync.
-///
-/// This information is stored in the [`ComponentInfo`] of the associated component.
-///
-/// There is two ways of configuring hooks for a component:
-/// 1. Defining the relevant hooks on the [`Component`] implementation
-/// 2. Using the [`World::register_component_hooks`] method
-///
-/// # Example 2
-///
-/// ```
-/// use bevy_ecs::prelude::*;
-/// use bevy_platform::collections::HashSet;
-///
-/// #[derive(Component)]
-/// struct MyTrackedComponent;
-///
-/// #[derive(Resource, Default)]
-/// struct TrackedEntities(HashSet);
-///
-/// let mut world = World::new();
-/// world.init_resource::();
-///
-/// // No entities with `MyTrackedComponent` have been added yet, so we can safely add component hooks
-/// let mut tracked_component_query = world.query::<&MyTrackedComponent>();
-/// assert!(tracked_component_query.iter(&world).next().is_none());
-///
-/// world.register_component_hooks::().on_add(|mut world, context| {
-/// let mut tracked_entities = world.resource_mut::();
-/// tracked_entities.0.insert(context.entity);
-/// });
-///
-/// world.register_component_hooks::().on_remove(|mut world, context| {
-/// let mut tracked_entities = world.resource_mut::();
-/// tracked_entities.0.remove(&context.entity);
-/// });
-///
-/// let entity = world.spawn(MyTrackedComponent).id();
-/// let tracked_entities = world.resource::();
-/// assert!(tracked_entities.0.contains(&entity));
-///
-/// world.despawn(entity);
-/// let tracked_entities = world.resource::();
-/// assert!(!tracked_entities.0.contains(&entity));
-/// ```
-#[derive(Debug, Clone, Default)]
-pub struct ComponentHooks {
- pub(crate) on_add: Option,
- pub(crate) on_insert: Option,
- pub(crate) on_replace: Option,
- pub(crate) on_remove: Option,
- pub(crate) on_despawn: Option,
-}
-
-impl ComponentHooks {
- pub(crate) fn update_from_component(&mut self) -> &mut Self {
- if let Some(hook) = C::on_add() {
- self.on_add(hook);
- }
- if let Some(hook) = C::on_insert() {
- self.on_insert(hook);
- }
- if let Some(hook) = C::on_replace() {
- self.on_replace(hook);
- }
- if let Some(hook) = C::on_remove() {
- self.on_remove(hook);
- }
- if let Some(hook) = C::on_despawn() {
- self.on_despawn(hook);
- }
-
- self
- }
-
- /// Register a [`ComponentHook`] that will be run when this component is added to an entity.
- /// An `on_add` hook will always run before `on_insert` hooks. Spawning an entity counts as
- /// adding all of its components.
- ///
- /// # Panics
- ///
- /// Will panic if the component already has an `on_add` hook
- pub fn on_add(&mut self, hook: ComponentHook) -> &mut Self {
- self.try_on_add(hook)
- .expect("Component already has an on_add hook")
- }
-
- /// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`)
- /// or replaced.
- ///
- /// An `on_insert` hook always runs after any `on_add` hooks (if the entity didn't already have the component).
- ///
- /// # Warning
- ///
- /// The hook won't run if the component is already present and is only mutated, such as in a system via a query.
- /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches.
- ///
- /// # Panics
- ///
- /// Will panic if the component already has an `on_insert` hook
- pub fn on_insert(&mut self, hook: ComponentHook) -> &mut Self {
- self.try_on_insert(hook)
- .expect("Component already has an on_insert hook")
- }
-
- /// Register a [`ComponentHook`] that will be run when this component is about to be dropped,
- /// such as being replaced (with `.insert`) or removed.
- ///
- /// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced,
- /// allowing access to the previous data just before it is dropped.
- /// This hook does *not* run if the entity did not already have this component.
- ///
- /// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity).
- ///
- /// # Warning
- ///
- /// The hook won't run if the component is already present and is only mutated, such as in a system via a query.
- /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches.
- ///
- /// # Panics
- ///
- /// Will panic if the component already has an `on_replace` hook
- pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self {
- self.try_on_replace(hook)
- .expect("Component already has an on_replace hook")
- }
-
- /// Register a [`ComponentHook`] that will be run when this component is removed from an entity.
- /// Despawning an entity counts as removing all of its components.
- ///
- /// # Panics
- ///
- /// Will panic if the component already has an `on_remove` hook
- pub fn on_remove(&mut self, hook: ComponentHook) -> &mut Self {
- self.try_on_remove(hook)
- .expect("Component already has an on_remove hook")
- }
-
- /// Register a [`ComponentHook`] that will be run for each component on an entity when it is despawned.
- ///
- /// # Panics
- ///
- /// Will panic if the component already has an `on_despawn` hook
- pub fn on_despawn(&mut self, hook: ComponentHook) -> &mut Self {
- self.try_on_despawn(hook)
- .expect("Component already has an on_despawn hook")
- }
-
- /// Attempt to register a [`ComponentHook`] that will be run when this component is added to an entity.
- ///
- /// This is a fallible version of [`Self::on_add`].
- ///
- /// Returns `None` if the component already has an `on_add` hook.
- pub fn try_on_add(&mut self, hook: ComponentHook) -> Option<&mut Self> {
- if self.on_add.is_some() {
- return None;
- }
- self.on_add = Some(hook);
- Some(self)
- }
-
- /// Attempt to register a [`ComponentHook`] that will be run when this component is added (with `.insert`)
- ///
- /// This is a fallible version of [`Self::on_insert`].
- ///
- /// Returns `None` if the component already has an `on_insert` hook.
- pub fn try_on_insert(&mut self, hook: ComponentHook) -> Option<&mut Self> {
- if self.on_insert.is_some() {
- return None;
- }
- self.on_insert = Some(hook);
- Some(self)
- }
-
- /// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed
- ///
- /// This is a fallible version of [`Self::on_replace`].
- ///
- /// Returns `None` if the component already has an `on_replace` hook.
- pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> {
- if self.on_replace.is_some() {
- return None;
- }
- self.on_replace = Some(hook);
- Some(self)
- }
-
- /// Attempt to register a [`ComponentHook`] that will be run when this component is removed from an entity.
- ///
- /// This is a fallible version of [`Self::on_remove`].
- ///
- /// Returns `None` if the component already has an `on_remove` hook.
- pub fn try_on_remove(&mut self, hook: ComponentHook) -> Option<&mut Self> {
- if self.on_remove.is_some() {
- return None;
- }
- self.on_remove = Some(hook);
- Some(self)
- }
-
- /// Attempt to register a [`ComponentHook`] that will be run for each component on an entity when it is despawned.
- ///
- /// This is a fallible version of [`Self::on_despawn`].
- ///
- /// Returns `None` if the component already has an `on_despawn` hook.
- pub fn try_on_despawn(&mut self, hook: ComponentHook) -> Option<&mut Self> {
- if self.on_despawn.is_some() {
- return None;
- }
- self.on_despawn = Some(hook);
- Some(self)
- }
-}
-
/// Stores metadata for a type of component or resource stored in a specific [`World`].
#[derive(Debug, Clone)]
pub struct ComponentInfo {
@@ -913,8 +677,8 @@ impl ComponentInfo {
/// Returns the name of the current component.
#[inline]
- pub fn name(&self) -> &str {
- &self.descriptor.name
+ pub fn name(&self) -> DebugName {
+ self.descriptor.name.clone()
}
/// Returns `true` if the current component is mutable.
@@ -1071,7 +835,7 @@ impl SparseSetIndex for ComponentId {
/// A value describing a component or resource, which may or may not correspond to a Rust type.
#[derive(Clone)]
pub struct ComponentDescriptor {
- name: Cow<'static, str>,
+ name: DebugName,
// SAFETY: This must remain private. It must match the statically known StorageType of the
// associated rust component type if one exists.
storage_type: StorageType,
@@ -1117,7 +881,7 @@ impl ComponentDescriptor {
/// Create a new `ComponentDescriptor` for the type `T`.
pub fn new() -> Self {
Self {
- name: Cow::Borrowed(core::any::type_name::()),
+ name: DebugName::type_name::(),
storage_type: T::STORAGE_TYPE,
is_send_and_sync: true,
type_id: Some(TypeId::of::()),
@@ -1142,7 +906,7 @@ impl ComponentDescriptor {
clone_behavior: ComponentCloneBehavior,
) -> Self {
Self {
- name: name.into(),
+ name: name.into().into(),
storage_type,
is_send_and_sync: true,
type_id: None,
@@ -1158,7 +922,7 @@ impl ComponentDescriptor {
/// The [`StorageType`] for resources is always [`StorageType::Table`].
pub fn new_resource() -> Self {
Self {
- name: Cow::Borrowed(core::any::type_name::()),
+ name: DebugName::type_name::(),
// PERF: `SparseStorage` may actually be a more
// reasonable choice as `storage_type` for resources.
storage_type: StorageType::Table,
@@ -1173,7 +937,7 @@ impl ComponentDescriptor {
fn new_non_send(storage_type: StorageType) -> Self {
Self {
- name: Cow::Borrowed(core::any::type_name::()),
+ name: DebugName::type_name::(),
storage_type,
is_send_and_sync: false,
type_id: Some(TypeId::of::()),
@@ -1199,8 +963,8 @@ impl ComponentDescriptor {
/// Returns the name of the current component.
#[inline]
- pub fn name(&self) -> &str {
- self.name.as_ref()
+ pub fn name(&self) -> DebugName {
+ self.name.clone()
}
/// Returns whether this component is mutable.
@@ -2052,7 +1816,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]
@@ -2089,13 +1853,10 @@ impl Components {
///
/// 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]
- pub fn get_name<'a>(&'a self, id: ComponentId) -> Option> {
+ pub fn get_name<'a>(&'a self, id: ComponentId) -> Option