From 57ddae1e93543f8a0b0d719c776b89129059d7ca Mon Sep 17 00:00:00 2001 From: Talin Date: Tue, 10 Jun 2025 09:50:08 -0700 Subject: [PATCH 001/106] Core button widget (#19366) # Objective Part of #19236 ## Solution Adds a new `bevy_core_widgets` crate containing headless widget implementations. This PR adds a single `CoreButton` widget, more widgets to be added later once this is approved. ## Testing There's an example, ui/core_widgets. --------- Co-authored-by: Alice Cecile --- Cargo.toml | 26 ++ crates/bevy_core_widgets/Cargo.toml | 32 ++ crates/bevy_core_widgets/src/core_button.rs | 141 ++++++++ crates/bevy_core_widgets/src/lib.rs | 27 ++ crates/bevy_internal/Cargo.toml | 1 + crates/bevy_internal/src/lib.rs | 2 + crates/bevy_picking/src/hover.rs | 286 ++++++++++++++++- crates/bevy_picking/src/lib.rs | 9 +- crates/bevy_ui/src/interaction_states.rs | 74 +++++ crates/bevy_ui/src/lib.rs | 7 + crates/bevy_ui/src/ui_node.rs | 10 + docs/cargo_features.md | 1 + examples/README.md | 2 + examples/ui/core_widgets.rs | 233 ++++++++++++++ examples/ui/core_widgets_observers.rs | 303 ++++++++++++++++++ .../release-notes/headless-widgets.md | 90 ++++++ 16 files changed, 1242 insertions(+), 2 deletions(-) create mode 100644 crates/bevy_core_widgets/Cargo.toml create mode 100644 crates/bevy_core_widgets/src/core_button.rs create mode 100644 crates/bevy_core_widgets/src/lib.rs create mode 100644 crates/bevy_ui/src/interaction_states.rs create mode 100644 examples/ui/core_widgets.rs create mode 100644 examples/ui/core_widgets_observers.rs create mode 100644 release-content/release-notes/headless-widgets.md diff --git a/Cargo.toml b/Cargo.toml index 703877983c..32891dd1be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,7 @@ default = [ "bevy_audio", "bevy_color", "bevy_core_pipeline", + "bevy_core_widgets", "bevy_anti_aliasing", "bevy_gilrs", "bevy_gizmos", @@ -292,6 +293,9 @@ bevy_log = ["bevy_internal/bevy_log"] # Enable input focus subsystem bevy_input_focus = ["bevy_internal/bevy_input_focus"] +# Headless widget collection for Bevy UI. +bevy_core_widgets = ["bevy_internal/bevy_core_widgets"] + # Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation) spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"] @@ -4438,3 +4442,25 @@ name = "Hotpatching Systems" description = "Demonstrates how to hotpatch systems" category = "ECS (Entity Component System)" wasm = false + +[[example]] +name = "core_widgets" +path = "examples/ui/core_widgets.rs" +doc-scrape-examples = true + +[package.metadata.example.core_widgets] +name = "Core Widgets" +description = "Demonstrates use of core (headless) widgets in Bevy UI" +category = "UI (User Interface)" +wasm = true + +[[example]] +name = "core_widgets_observers" +path = "examples/ui/core_widgets_observers.rs" +doc-scrape-examples = true + +[package.metadata.example.core_widgets_observers] +name = "Core Widgets (w/Observers)" +description = "Demonstrates use of core (headless) widgets in Bevy UI, with Observers" +category = "UI (User Interface)" +wasm = true diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml new file mode 100644 index 0000000000..21540a9787 --- /dev/null +++ b/crates/bevy_core_widgets/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "bevy_core_widgets" +version = "0.16.0-dev" +edition = "2024" +description = "Unstyled common widgets for B Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } +bevy_a11y = { path = "../bevy_a11y", version = "0.16.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" } +bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev" } + +# other +accesskit = "0.19" + +[features] +default = [] + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs new file mode 100644 index 0000000000..23f5c28380 --- /dev/null +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -0,0 +1,141 @@ +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::query::Has; +use bevy_ecs::system::ResMut; +use bevy_ecs::{ + component::Component, + entity::Entity, + observer::Trigger, + query::With, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; +use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Pressed, Released}; +use bevy_ui::{Depressed, InteractionDisabled}; + +/// Headless button widget. This widget maintains a "pressed" state, which is used to +/// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked` +/// event when the button is un-pressed. +#[derive(Component, Debug)] +#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))] +pub struct CoreButton { + /// Optional system to run when the button is clicked, or when the Enter or Space key + /// is pressed while the button is focused. If this field is `None`, the button will + /// emit a `ButtonClicked` event when clicked. + pub on_click: Option, +} + +fn button_on_key_event( + mut trigger: Trigger>, + q_state: Query<(&CoreButton, Has)>, + mut commands: Commands, +) { + if let Ok((bstate, disabled)) = q_state.get(trigger.target().unwrap()) { + if !disabled { + let event = &trigger.event().input; + if !event.repeat + && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) + { + if let Some(on_click) = bstate.on_click { + trigger.propagate(false); + commands.run_system(on_click); + } + } + } + } +} + +fn button_on_pointer_click( + mut trigger: Trigger>, + mut q_state: Query<(&CoreButton, Has, Has)>, + mut commands: Commands, +) { + if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target().unwrap()) { + trigger.propagate(false); + if pressed && !disabled { + if let Some(on_click) = bstate.on_click { + commands.run_system(on_click); + } + } + } +} + +fn button_on_pointer_down( + mut trigger: Trigger>, + mut q_state: Query<(Entity, Has, Has), With>, + focus: Option>, + focus_visible: Option>, + mut commands: Commands, +) { + if let Ok((button, disabled, depressed)) = q_state.get_mut(trigger.target().unwrap()) { + trigger.propagate(false); + if !disabled { + if !depressed { + commands.entity(button).insert(Depressed); + } + // Clicking on a button makes it the focused input, + // and hides the focus ring if it was visible. + if let Some(mut focus) = focus { + focus.0 = trigger.target(); + } + if let Some(mut focus_visible) = focus_visible { + focus_visible.0 = false; + } + } + } +} + +fn button_on_pointer_up( + mut trigger: Trigger>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, depressed)) = q_state.get_mut(trigger.target().unwrap()) { + trigger.propagate(false); + if !disabled && depressed { + commands.entity(button).remove::(); + } + } +} + +fn button_on_pointer_drag_end( + mut trigger: Trigger>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, depressed)) = q_state.get_mut(trigger.target().unwrap()) { + trigger.propagate(false); + if !disabled && depressed { + commands.entity(button).remove::(); + } + } +} + +fn button_on_pointer_cancel( + mut trigger: Trigger>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, depressed)) = q_state.get_mut(trigger.target().unwrap()) { + trigger.propagate(false); + if !disabled && depressed { + commands.entity(button).remove::(); + } + } +} + +/// Plugin that adds the observers for the [`CoreButton`] widget. +pub struct CoreButtonPlugin; + +impl Plugin for CoreButtonPlugin { + fn build(&self, app: &mut App) { + app.add_observer(button_on_key_event) + .add_observer(button_on_pointer_down) + .add_observer(button_on_pointer_up) + .add_observer(button_on_pointer_click) + .add_observer(button_on_pointer_drag_end) + .add_observer(button_on_pointer_cancel); + } +} diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs new file mode 100644 index 0000000000..afeed92a1f --- /dev/null +++ b/crates/bevy_core_widgets/src/lib.rs @@ -0,0 +1,27 @@ +//! This crate provides a set of core widgets for Bevy UI, such as buttons, checkboxes, and sliders. +//! These widgets have no inherent styling, it's the responsibility of the user to add styling +//! appropriate for their game or application. +//! +//! # State Management +//! +//! Most of the widgets use external state management: this means that the widgets do not +//! automatically update their own internal state, but instead rely on the app to update the widget +//! state (as well as any other related game state) in response to a change event emitted by the +//! widget. The primary motivation for this is to avoid two-way data binding in scenarios where the +//! user interface is showing a live view of dynamic data coming from deeper within the game engine. + +mod core_button; + +use bevy_app::{App, Plugin}; + +pub use core_button::{CoreButton, CoreButtonPlugin}; + +/// A plugin that registers the observers for all of the core widgets. If you don't want to +/// use all of the widgets, you can import the individual widget plugins instead. +pub struct CoreWidgetsPlugin; + +impl Plugin for CoreWidgetsPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(CoreButtonPlugin); + } +} diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 9f78fc009d..e22702e348 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -395,6 +395,7 @@ bevy_color = { path = "../bevy_color", optional = true, version = "0.16.0-dev", "bevy_reflect", ] } bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.16.0-dev" } +bevy_core_widgets = { path = "../bevy_core_widgets", optional = true, version = "0.16.0-dev" } bevy_anti_aliasing = { path = "../bevy_anti_aliasing", optional = true, version = "0.16.0-dev" } bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.16.0-dev" } bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.16.0-dev" } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 274364882e..b5446d6b85 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -29,6 +29,8 @@ pub use bevy_audio as audio; pub use bevy_color as color; #[cfg(feature = "bevy_core_pipeline")] pub use bevy_core_pipeline as core_pipeline; +#[cfg(feature = "bevy_core_widgets")] +pub use bevy_core_widgets as core_widgets; #[cfg(feature = "bevy_dev_tools")] pub use bevy_dev_tools as dev_tools; pub use bevy_diagnostic as diagnostic; diff --git a/crates/bevy_picking/src/hover.rs b/crates/bevy_picking/src/hover.rs index 2bf23c50ba..529cb94e8c 100644 --- a/crates/bevy_picking/src/hover.rs +++ b/crates/bevy_picking/src/hover.rs @@ -14,7 +14,7 @@ use crate::{ }; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::prelude::*; +use bevy_ecs::{entity::EntityHashSet, prelude::*}; use bevy_math::FloatOrd; use bevy_platform::collections::HashMap; use bevy_reflect::prelude::*; @@ -279,3 +279,287 @@ fn merge_interaction_states( new_interaction_state.insert(*hovered_entity, new_interaction); } } + +/// A component that allows users to use regular Bevy change detection to determine when the pointer +/// enters or leaves an entity. Users should insert this component on an entity to indicate interest +/// in knowing about hover state changes. +/// +/// The component's boolean value will be `true` whenever the pointer is currently directly hovering +/// over the entity, or any of the entity's descendants (as defined by the [`ChildOf`] +/// relationship). This is consistent with the behavior of the CSS `:hover` pseudo-class, which +/// applies to the element and all of its descendants. +/// +/// The contained boolean value is guaranteed to only be mutated when the pointer enters or leaves +/// the entity, allowing Bevy change detection to be used efficiently. This is in contrast to the +/// [`HoverMap`] resource, which is updated every frame. +/// +/// Typically, a simple hoverable entity or widget will have this component added to it. More +/// complex widgets can have this component added to each hoverable part. +/// +/// The computational cost of keeping the `IsHovered` components up to date is relatively cheap, and +/// linear in the number of entities that have the [`IsHovered`] component inserted. +#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[component(immutable)] +pub struct IsHovered(pub bool); + +impl IsHovered { + /// Get whether the entity is currently hovered. + pub fn get(&self) -> bool { + self.0 + } +} + +/// A component that allows users to use regular Bevy change detection to determine when the pointer +/// is directly hovering over an entity. Users should insert this component on an entity to indicate +/// interest in knowing about hover state changes. +/// +/// This is similar to [`IsHovered`] component, except that it does not include descendants in the +/// hover state. +#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[component(immutable)] +pub struct IsDirectlyHovered(pub bool); + +impl IsDirectlyHovered { + /// Get whether the entity is currently hovered. + pub fn get(&self) -> bool { + self.0 + } +} + +/// Uses [`HoverMap`] changes to update [`IsHovered`] components. +pub fn update_is_hovered( + hover_map: Option>, + mut hovers: Query<(Entity, &IsHovered)>, + parent_query: Query<&ChildOf>, + mut commands: Commands, +) { + // Don't do any work if there's no hover map. + let Some(hover_map) = hover_map else { return }; + + // Don't bother collecting ancestors if there are no hovers. + if hovers.is_empty() { + return; + } + + // Algorithm: for each entity having a `IsHovered` component, we want to know if the current + // entry in the hover map is "within" (that is, in the set of descenants of) that entity. Rather + // than doing an expensive breadth-first traversal of children, instead start with the hovermap + // entry and search upwards. We can make this even cheaper by building a set of ancestors for + // the hovermap entry, and then testing each `IsHovered` entity against that set. + + // A set which contains the hovered for the current pointer entity and its ancestors. The + // capacity is based on the likely tree depth of the hierarchy, which is typically greater for + // UI (because of layout issues) than for 3D scenes. A depth of 32 is a reasonable upper bound + // for most use cases. + let mut hover_ancestors = EntityHashSet::with_capacity(32); + if let Some(map) = hover_map.get(&PointerId::Mouse) { + for hovered_entity in map.keys() { + hover_ancestors.insert(*hovered_entity); + hover_ancestors.extend(parent_query.iter_ancestors(*hovered_entity)); + } + } + + // For each hovered entity, it is considered "hovering" if it's in the set of hovered ancestors. + for (entity, hoverable) in hovers.iter_mut() { + let is_hovering = hover_ancestors.contains(&entity); + if hoverable.0 != is_hovering { + commands.entity(entity).insert(IsHovered(is_hovering)); + } + } +} + +/// Uses [`HoverMap`] changes to update [`IsDirectlyHovered`] components. +pub fn update_is_directly_hovered( + hover_map: Option>, + hovers: Query<(Entity, &IsDirectlyHovered)>, + mut commands: Commands, +) { + // Don't do any work if there's no hover map. + let Some(hover_map) = hover_map else { return }; + + // Don't bother collecting ancestors if there are no hovers. + if hovers.is_empty() { + return; + } + + if let Some(map) = hover_map.get(&PointerId::Mouse) { + // It's hovering if it's in the HoverMap. + for (entity, hoverable) in hovers.iter() { + let is_hovering = map.contains_key(&entity); + if hoverable.0 != is_hovering { + commands + .entity(entity) + .insert(IsDirectlyHovered(is_hovering)); + } + } + } else { + // No hovered entity, reset all hovers. + for (entity, hoverable) in hovers.iter() { + if hoverable.0 { + commands.entity(entity).insert(IsDirectlyHovered(false)); + } + } + } +} + +#[cfg(test)] +mod tests { + use bevy_render::camera::Camera; + + use super::*; + + #[test] + fn update_is_hovered_memoized() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_child = world.spawn_empty().id(); + let hovered_entity = world.spawn(IsHovered(false)).add_child(hovered_child).id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_child, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_hovered).is_ok()); + + // Check to insure that the hovered entity has the IsHovered component set to true + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(hover.get()); + assert!(hover.is_changed()); + + // Now do it again, but don't change the hover map. + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_hovered).is_ok()); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(hover.get()); + + // Should not be changed + // NOTE: Test doesn't work - thinks it is always changed + // assert!(!hover.is_changed()); + + // Clear the hover map and run again. + world.insert_resource(HoverMap::default()); + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_hovered).is_ok()); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } + + #[test] + fn update_is_hovered_direct_self() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_entity = world.spawn(IsDirectlyHovered(false)).id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_entity, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + + // Check to insure that the hovered entity has the IsDirectlyHovered component set to true + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(hover.get()); + assert!(hover.is_changed()); + + // Now do it again, but don't change the hover map. + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(hover.get()); + + // Should not be changed + // NOTE: Test doesn't work - thinks it is always changed + // assert!(!hover.is_changed()); + + // Clear the hover map and run again. + world.insert_resource(HoverMap::default()); + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } + + #[test] + fn update_is_hovered_direct_child() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_child = world.spawn_empty().id(); + let hovered_entity = world + .spawn(IsDirectlyHovered(false)) + .add_child(hovered_child) + .id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_child, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + + // Check to insure that the IsDirectlyHovered component is still false + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } +} diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 70a5714581..fde5801b42 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -170,6 +170,7 @@ pub mod window; use bevy_app::{prelude::*, PluginGroupBuilder}; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; +use hover::{update_is_directly_hovered, update_is_hovered}; /// The picking prelude. /// @@ -392,6 +393,7 @@ impl Plugin for PickingPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -429,7 +431,12 @@ impl Plugin for InteractionPlugin { .add_event::>() .add_systems( PreUpdate, - (generate_hovermap, update_interactions, pointer_events) + ( + generate_hovermap, + update_interactions, + (update_is_hovered, update_is_directly_hovered), + pointer_events, + ) .chain() .in_set(PickingSystems::Hover), ); diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs new file mode 100644 index 0000000000..eed4864258 --- /dev/null +++ b/crates/bevy_ui/src/interaction_states.rs @@ -0,0 +1,74 @@ +/// This module contains components that are used to track the interaction state of UI widgets. +use bevy_a11y::AccessibilityNode; +use bevy_ecs::{ + component::Component, + lifecycle::{OnAdd, OnInsert, OnRemove}, + observer::Trigger, + world::DeferredWorld, +}; + +/// A component indicating that a widget is disabled and should be "grayed out". +/// This is used to prevent user interaction with the widget. It should not, however, prevent +/// the widget from being updated or rendered, or from acquiring keyboard focus. +/// +/// For apps which support a11y: if a widget (such as a slider) contains multiple entities, +/// the `InteractionDisabled` component should be added to the root entity of the widget - the +/// same entity that contains the `AccessibilityNode` component. This will ensure that +/// the a11y tree is updated correctly. +#[derive(Component, Debug, Clone, Copy, Default)] +pub struct InteractionDisabled; + +pub(crate) fn on_add_disabled( + trigger: Trigger, + mut world: DeferredWorld, +) { + let mut entity = world.entity_mut(trigger.target().unwrap()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_disabled(); + } +} + +pub(crate) fn on_remove_disabled( + trigger: Trigger, + mut world: DeferredWorld, +) { + let mut entity = world.entity_mut(trigger.target().unwrap()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.clear_disabled(); + } +} + +/// Component that indicates whether a button or widget is currently in a pressed or "held down" +/// state. +#[derive(Component, Default, Debug)] +pub struct Depressed; + +/// Component that indicates whether a checkbox or radio button is in a checked state. +#[derive(Component, Default, Debug)] +#[component(immutable)] +pub struct Checked(pub bool); + +impl Checked { + /// Returns whether the checkbox or radio button is currently checked. + pub fn get(&self) -> bool { + self.0 + } +} + +pub(crate) fn on_insert_checked(trigger: Trigger, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target().unwrap()); + let checked = entity.get::().unwrap().get(); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(match checked { + true => accesskit::Toggled::True, + false => accesskit::Toggled::False, + }); + } +} + +pub(crate) fn on_remove_checked(trigger: Trigger, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target().unwrap()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(accesskit::Toggled::False); + } +} diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index be0649c038..6edfad0967 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -10,6 +10,7 @@ //! Spawn UI elements with [`widget::Button`], [`ImageNode`], [`Text`](prelude::Text) and [`Node`] //! This UI is laid out with the Flexbox and CSS Grid layout models (see ) +pub mod interaction_states; pub mod measurement; pub mod ui_material; pub mod update; @@ -38,6 +39,7 @@ mod ui_node; pub use focus::*; pub use geometry::*; pub use gradients::*; +pub use interaction_states::{Checked, Depressed, InteractionDisabled}; pub use layout::*; pub use measurement::*; pub use render::*; @@ -319,6 +321,11 @@ fn build_text_interop(app: &mut App) { app.add_plugins(accessibility::AccessibilityPlugin); + app.add_observer(interaction_states::on_add_disabled) + .add_observer(interaction_states::on_remove_disabled) + .add_observer(interaction_states::on_insert_checked) + .add_observer(interaction_states::on_remove_checked); + app.configure_sets( PostUpdate, AmbiguousWithText.ambiguous_with(widget::text_system), diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 2ae820a1a1..1c5ed364b9 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -2130,6 +2130,16 @@ impl BorderColor { } } + /// Helper to set all border colors to a given color. + pub fn set_all(&mut self, color: impl Into) -> &mut Self { + let color: Color = color.into(); + self.top = color; + self.bottom = color; + self.left = color; + self.right = color; + self + } + /// Check if all contained border colors are transparent pub fn is_fully_transparent(&self) -> bool { self.top.is_fully_transparent() diff --git a/docs/cargo_features.md b/docs/cargo_features.md index e0f00f2f3d..8784d5e725 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -21,6 +21,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_audio|Provides audio functionality| |bevy_color|Provides shared color types and operations| |bevy_core_pipeline|Provides cameras and other basic render pipeline features| +|bevy_core_widgets|Headless widget collection for Bevy UI.| |bevy_gilrs|Adds gamepad support| |bevy_gizmos|Adds support for rendering gizmos| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support| diff --git a/examples/README.md b/examples/README.md index dce7c114e8..4e679d0d7a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -545,6 +545,8 @@ Example | Description [Box Shadow](../examples/ui/box_shadow.rs) | Demonstrates how to create a node with a shadow [Button](../examples/ui/button.rs) | Illustrates creating and updating a button [CSS Grid](../examples/ui/grid.rs) | An example for CSS Grid layout +[Core Widgets](../examples/ui/core_widgets.rs) | Demonstrates use of core (headless) widgets in Bevy UI +[Core Widgets (w/Observers)](../examples/ui/core_widgets_observers.rs) | Demonstrates use of core (headless) widgets in Bevy UI, with Observers [Directional Navigation](../examples/ui/directional_navigation.rs) | Demonstration of Directional Navigation between UI elements [Display and Visibility](../examples/ui/display_and_visibility.rs) | Demonstrates how Display and Visibility work in the UI. [Flex Layout](../examples/ui/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text diff --git a/examples/ui/core_widgets.rs b/examples/ui/core_widgets.rs new file mode 100644 index 0000000000..69a45f3e50 --- /dev/null +++ b/examples/ui/core_widgets.rs @@ -0,0 +1,233 @@ +//! This example illustrates how to create widgets using the `bevy_core_widgets` widget set. + +use bevy::{ + color::palettes::basic::*, + core_widgets::{CoreButton, CoreWidgetsPlugin}, + ecs::system::SystemId, + input_focus::{ + tab_navigation::{TabGroup, TabIndex}, + InputDispatchPlugin, + }, + picking::hover::IsHovered, + prelude::*, + ui::{Depressed, InteractionDisabled}, + winit::WinitSettings, +}; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin)) + // Only run the app when there is user input. This will significantly reduce CPU/GPU use. + .insert_resource(WinitSettings::desktop_app()) + .add_systems(Startup, setup) + .add_systems( + Update, + (update_button_style, update_button_style2, toggle_disabled), + ) + .run(); +} + +const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); +const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); +const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); + +/// Marker which identifies buttons with a particular style, in this case the "Demo style". +#[derive(Component)] +struct DemoButton; + +fn update_button_style( + mut buttons: Query< + ( + Has, + &IsHovered, + Has, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + ( + Or<( + Changed, + Changed, + Added, + )>, + With, + ), + >, + mut text_query: Query<&mut Text>, +) { + for (depressed, hovered, disabled, mut color, mut border_color, children) in &mut buttons { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + disabled, + hovered.get(), + depressed, + &mut color, + &mut border_color, + &mut text, + ); + } +} + +/// Supplementary system to detect removed marker components +fn update_button_style2( + mut buttons: Query< + ( + Has, + &IsHovered, + Has, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + With, + >, + mut removed_depressed: RemovedComponents, + mut removed_disabled: RemovedComponents, + mut text_query: Query<&mut Text>, +) { + removed_depressed.read().for_each(|entity| { + if let Ok((depressed, hovered, disabled, mut color, mut border_color, children)) = + buttons.get_mut(entity) + { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + disabled, + hovered.get(), + depressed, + &mut color, + &mut border_color, + &mut text, + ); + } + }); + removed_disabled.read().for_each(|entity| { + if let Ok((depressed, hovered, disabled, mut color, mut border_color, children)) = + buttons.get_mut(entity) + { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + disabled, + hovered.get(), + depressed, + &mut color, + &mut border_color, + &mut text, + ); + } + }); +} + +fn set_button_style( + disabled: bool, + hovered: bool, + depressed: bool, + color: &mut BackgroundColor, + border_color: &mut BorderColor, + text: &mut Text, +) { + match (disabled, hovered, depressed) { + // Disabled button + (true, _, _) => { + **text = "Disabled".to_string(); + *color = NORMAL_BUTTON.into(); + border_color.set_all(GRAY); + } + + // Pressed and hovered button + (false, true, true) => { + **text = "Press".to_string(); + *color = PRESSED_BUTTON.into(); + border_color.set_all(RED); + } + + // Hovered, unpressed button + (false, true, false) => { + **text = "Hover".to_string(); + *color = HOVERED_BUTTON.into(); + border_color.set_all(WHITE); + } + + // Unhovered button (either pressed or not). + (false, false, _) => { + **text = "Button".to_string(); + *color = NORMAL_BUTTON.into(); + border_color.set_all(BLACK); + } + } +} + +fn setup(mut commands: Commands, assets: Res) { + let on_click = commands.register_system(|| { + info!("Button clicked!"); + }); + // ui camera + commands.spawn(Camera2d); + commands.spawn(button(&assets, on_click)); +} + +fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { + ( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + display: Display::Flex, + flex_direction: FlexDirection::Column, + ..default() + }, + TabGroup::default(), + children![ + ( + Node { + width: Val::Px(150.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + DemoButton, + CoreButton { + on_click: Some(on_click), + }, + IsHovered::default(), + TabIndex(0), + BorderColor::all(Color::BLACK), + BorderRadius::MAX, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Button"), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + TextShadow::default(), + )] + ), + Text::new("Press 'D' to toggle button disabled state"), + ], + ) +} + +fn toggle_disabled( + input: Res>, + mut interaction_query: Query<(Entity, Has), With>, + mut commands: Commands, +) { + if input.just_pressed(KeyCode::KeyD) { + for (entity, disabled) in &mut interaction_query { + // disabled.0 = !disabled.0; + if disabled { + info!("Button enabled"); + commands.entity(entity).remove::(); + } else { + info!("Button disabled"); + commands.entity(entity).insert(InteractionDisabled); + } + } + } +} diff --git a/examples/ui/core_widgets_observers.rs b/examples/ui/core_widgets_observers.rs new file mode 100644 index 0000000000..55dc2d36b1 --- /dev/null +++ b/examples/ui/core_widgets_observers.rs @@ -0,0 +1,303 @@ +//! This example illustrates how to create widgets using the `bevy_core_widgets` widget set. + +use bevy::{ + color::palettes::basic::*, + core_widgets::{CoreButton, CoreWidgetsPlugin}, + ecs::system::SystemId, + input_focus::{ + tab_navigation::{TabGroup, TabIndex}, + InputDispatchPlugin, + }, + picking::hover::IsHovered, + prelude::*, + ui::{Depressed, InteractionDisabled}, + winit::WinitSettings, +}; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin)) + // Only run the app when there is user input. This will significantly reduce CPU/GPU use. + .insert_resource(WinitSettings::desktop_app()) + .add_systems(Startup, setup) + .add_observer(on_add_pressed) + .add_observer(on_remove_pressed) + .add_observer(on_add_disabled) + .add_observer(on_remove_disabled) + .add_observer(on_change_hover) + .add_systems(Update, toggle_disabled) + .run(); +} + +const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); +const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); +const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); + +/// Marker which identifies buttons with a particular style, in this case the "Demo style". +#[derive(Component)] +struct DemoButton; + +fn on_add_pressed( + trigger: Trigger, + mut buttons: Query< + ( + &IsHovered, + Has, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + With, + >, + mut text_query: Query<&mut Text>, +) { + if let Ok((hovered, disabled, mut color, mut border_color, children)) = + buttons.get_mut(trigger.target().unwrap()) + { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + disabled, + hovered.get(), + true, + &mut color, + &mut border_color, + &mut text, + ); + } +} + +fn on_remove_pressed( + trigger: Trigger, + mut buttons: Query< + ( + &IsHovered, + Has, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + With, + >, + mut text_query: Query<&mut Text>, +) { + if let Ok((hovered, disabled, mut color, mut border_color, children)) = + buttons.get_mut(trigger.target().unwrap()) + { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + disabled, + hovered.get(), + false, + &mut color, + &mut border_color, + &mut text, + ); + } +} + +fn on_add_disabled( + trigger: Trigger, + mut buttons: Query< + ( + Has, + &IsHovered, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + With, + >, + mut text_query: Query<&mut Text>, +) { + if let Ok((depressed, hovered, mut color, mut border_color, children)) = + buttons.get_mut(trigger.target().unwrap()) + { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + true, + hovered.get(), + depressed, + &mut color, + &mut border_color, + &mut text, + ); + } +} + +fn on_remove_disabled( + trigger: Trigger, + mut buttons: Query< + ( + Has, + &IsHovered, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + With, + >, + mut text_query: Query<&mut Text>, +) { + if let Ok((depressed, hovered, mut color, mut border_color, children)) = + buttons.get_mut(trigger.target().unwrap()) + { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + false, + hovered.get(), + depressed, + &mut color, + &mut border_color, + &mut text, + ); + } +} + +fn on_change_hover( + trigger: Trigger, + mut buttons: Query< + ( + Has, + &IsHovered, + Has, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + With, + >, + mut text_query: Query<&mut Text>, +) { + if let Ok((depressed, hovered, disabled, mut color, mut border_color, children)) = + buttons.get_mut(trigger.target().unwrap()) + { + if children.is_empty() { + return; + } + let Ok(mut text) = text_query.get_mut(children[0]) else { + return; + }; + set_button_style( + disabled, + hovered.get(), + depressed, + &mut color, + &mut border_color, + &mut text, + ); + } +} + +fn set_button_style( + disabled: bool, + hovered: bool, + depressed: bool, + color: &mut BackgroundColor, + border_color: &mut BorderColor, + text: &mut Text, +) { + match (disabled, hovered, depressed) { + // Disabled button + (true, _, _) => { + **text = "Disabled".to_string(); + *color = NORMAL_BUTTON.into(); + border_color.set_all(GRAY); + } + + // Pressed and hovered button + (false, true, true) => { + **text = "Press".to_string(); + *color = PRESSED_BUTTON.into(); + border_color.set_all(RED); + } + + // Hovered, unpressed button + (false, true, false) => { + **text = "Hover".to_string(); + *color = HOVERED_BUTTON.into(); + border_color.set_all(WHITE); + } + + // Unhovered button (either pressed or not). + (false, false, _) => { + **text = "Button".to_string(); + *color = NORMAL_BUTTON.into(); + border_color.set_all(BLACK); + } + } +} + +fn setup(mut commands: Commands, assets: Res) { + let on_click = commands.register_system(|| { + info!("Button clicked!"); + }); + // ui camera + commands.spawn(Camera2d); + commands.spawn(button(&assets, on_click)); +} + +fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { + ( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + display: Display::Flex, + flex_direction: FlexDirection::Column, + ..default() + }, + TabGroup::default(), + children![ + ( + Node { + width: Val::Px(150.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + DemoButton, + CoreButton { + on_click: Some(on_click), + }, + IsHovered::default(), + TabIndex(0), + BorderColor::all(Color::BLACK), + BorderRadius::MAX, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Button"), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + TextShadow::default(), + )] + ), + Text::new("Press 'D' to toggle button disabled state"), + ], + ) +} + +fn toggle_disabled( + input: Res>, + mut interaction_query: Query<(Entity, Has), With>, + mut commands: Commands, +) { + if input.just_pressed(KeyCode::KeyD) { + for (entity, disabled) in &mut interaction_query { + // disabled.0 = !disabled.0; + if disabled { + info!("Button enabled"); + commands.entity(entity).remove::(); + } else { + info!("Button disabled"); + commands.entity(entity).insert(InteractionDisabled); + } + } + } +} diff --git a/release-content/release-notes/headless-widgets.md b/release-content/release-notes/headless-widgets.md new file mode 100644 index 0000000000..a71e7aad2b --- /dev/null +++ b/release-content/release-notes/headless-widgets.md @@ -0,0 +1,90 @@ +--- +title: Headless Widgets +authors: ["@viridia"] +pull_requests: [19366] +--- + +Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately +these components have a number of shortcomings, such as the fact that they don't use the new +`bevy_picking` framework, or the fact that they are really only useful for creating buttons +and not other kinds of widgets like sliders. + +As an art form, games thrive on novelty: the typical game doesn't have boring, standardized controls +reminiscent of a productivity app, but instead will have beautiful, artistic widgets that are +in harmony with the game's overall visual theme. But writing new and unique widgets requires +skill and subtlety, particularly if we want first-class accessibility support. It's not a burden we +want to put on the average indie developer. + +In the web development world, "headless" widget libraries, such as +[headlessui](https://headlessui.com/) and [reakit](https://reakit.io/) have become popular. These +provide standardized widgets that implement all of the correct interactions and behavioral logic, +including integration with screen readers, but which are unstyled. It's the responsibility of the +game developer to provide the visual style and animation for the widgets, which can fit the overall +style of their game. + +With this release, Bevy introduces a collection of headless or "core" widgets. These are components +which can be added to any UI Node to get widget-like behavior. The core widget set includes buttons, +sliders, scrollbars, checkboxes, radio buttons, and more. This set will likely be expanded in +future releases. + +## Core Widgets + +The `bevy_core_widgets` crate provides implementations of unstyled widgets, such as buttons, +sliders, checkboxes and radio buttons. + +- `CoreButton` is a push button. It emits an activation event when clicked. +- (More to be added in subsequent PRs) + +## Widget Interaction States + +Many of the core widgets will define supplementary ECS components that are used to store the widget's +state, similar to how the old `Interaction` component worked, but in a way that is more flexible. +These components include: + +- `InteractionDisabled` - a boolean component used to indicate that a component should be + "grayed out" and non-interactive. Note that these disabled widgets are still visible and can + have keyboard focus (otherwise the user would have no way to discover them). +- `IsHovered` is a simple boolean component that allows detection of whether the widget is being + hovered using regular Bevy change detection. +- `Checked` is a boolean component that stores the checked state of a checkbox or radio button. +- `Depressed` is used for a button-like widget, and will be true while the button is held down. + +The combination of `IsHovered` and `ButtonPressed` fulfills the same purpose as the old +`Interaction` component, except that now we can also represent "roll-off" behavior (the state where +you click on a button and then, while holding the mouse down, move the pointer out of the button's +bounds). It also provides additional flexibility in cases where a widget has multiple hoverable +parts, or cases where a widget is hoverable but doesn't have a pressed state (such as a tree-view +expansion toggle). + +## Widget Notifications + +Applications need a way to be notified when the user interacts with a widget. One way to do this +is using Bevy observers. This approach is useful in cases where you want the widget notifications +to bubble up the hierarchy. + +However, in UI work it's often desirable to connect widget interactions in ways that cut across the +hierarchy. For these kinds of situations, the core widgets offer an an alternate approach: one-shot +systems. You can register a function as a one-shot system and get the resulting `SystemId`. This can +then be passed as a parameter to the widget when it is constructed, so when the button subsequently +gets clicked or the slider is dragged, the system gets run. Because it's an ECS system, it can +inject any additional parameters it needs to update the Bevy world in response to the interaction. + +Most of the core widgets use "external state management" - something that is referred to in the +React.js world as "controlled" widgets. This means that for widgets that edit a parameter value +(such as checkboxes and sliders), the widget doesn't automatically update its own internal value, +but only sends a notification to the app telling it that the value needs to change. It's the +responsibility of the app to handle this notification and update the widget accordingly, and at the +same time update any other game state that is dependent on that parameter. + +There are multiple reasons for this, but the main one is this: typical game user interfaces aren't +just passive forms of fields to fill in, but more often represent a dynamic view of live data. As a +consequence, the displayed value of a widget may change even when the user is not directly +interacting with that widget. Externalizing the state avoids the need for two-way data binding, and +instead allows simpler one-way data binding that aligns well with the traditional "Model / View / +Controller" (MVC) design pattern. + +There are two exceptions to this rule about external state management. First, widgets which don't +edit a value, but which merely trigger an event (such as buttons), don't fall under this rule. +Second, widgets which have complex states that are too large and heavyweight to fit within a +notification event (such as a text editor) can choose to manage their state internally. These latter +widgets will need to implement a two-way data binding strategy. From 8537718c6b8ff154687b8efea73da386699b4666 Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Tue, 10 Jun 2025 09:57:57 -0700 Subject: [PATCH 002/106] Fix `pbr` example text rotation (#19571) # Objective This example migration was missed in #16615 https://pixel-eagle.com/project/b25a040a-a980-4602-b90c-d480ab84076d/run/10633/compare/10627?screenshot=3D+Rendering/pbr.png ## Solution Use new `UiTransform` ## Testing `cargo run --example pbr` --- examples/3d/pbr.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/3d/pbr.rs b/examples/3d/pbr.rs index 12922db03b..da654ce1f3 100644 --- a/examples/3d/pbr.rs +++ b/examples/3d/pbr.rs @@ -85,8 +85,8 @@ fn setup( right: Val::ZERO, ..default() }, - Transform { - rotation: Quat::from_rotation_z(std::f32::consts::PI / 2.0), + UiTransform { + rotation: Rot2::degrees(90.), ..default() }, )); From 27b64c3bf15882761ac914c72bfdad675bebe84a Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 10 Jun 2025 19:01:37 +0200 Subject: [PATCH 003/106] Fix iOS simulator build (#19498) # Objective Fixes https://github.com/bevyengine/bevy/issues/18893. ## Solution `bindgen` currently has issues with building Xcode 16.3 and above, due to Clang being more strict about target triples. It was fixed in https://github.com/rust-lang/rust-bindgen/pull/3182, but as a breaking release, so affected dependencies have to be updated to either not depend on `bindgen`, or depend on a fixed version of it: - `tracing-oslog`: https://github.com/Absolucy/tracing-oslog/pull/12 - `coreaudio-sys`: https://github.com/RustAudio/coreaudio-sys/pull/114 ## Testing ```sh cargo build --target aarch64-apple-ios-sim ``` With Xcode 16.3 or 16.4. --- crates/bevy_audio/Cargo.toml | 6 ++++++ crates/bevy_log/Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) 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_log/Cargo.toml b/crates/bevy_log/Cargo.toml index e6d8899d63..fce602f7ad 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -45,7 +45,7 @@ bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = fa ] } [target.'cfg(target_os = "ios")'.dependencies] -tracing-oslog = "0.2" +tracing-oslog = "0.3" [lints] workspace = true From 33c6f45a35acf145a6244828a8f58cd9f45f3c9f Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Wed, 11 Jun 2025 00:57:28 +0300 Subject: [PATCH 004/106] Rename some pointer events and components (#19574) # Objective #19366 implemented core button widgets, which included the `Depressed` state component. `Depressed` was chosen instead of `Pressed` to avoid conflict with the `Pointer` event, but it is problematic and awkward in many ways: - Using the word "depressed" for such a high-traffic type is not great due to the obvious connection to "depressed" as in depression. - "Depressed" is not what I would search for if I was looking for a component like this, and I'm not aware of any other engine or UI framework using the term. - `Depressed` is not a very natural pair to the `Pointer` event. - It might be because I'm not a native English speaker, but I have very rarely heard someone say "a button is depressed". Seeing it, my mind initially goes from "depression??" to "oh, de-pressed, meaning released" and definitely not "is pressed", even though that *is* also a valid meaning for it. A related problem is that the current `Pointer` and `Pointer` event names use a different verb tense than all of our other observer events such as `Pointer` or `Pointer`. By fixing this and renaming `Pressed` (and `Released`), we can then use `Pressed` instead of `Depressed` for the state component. Additionally, the `IsHovered` and `IsDirectlyHovered` components added in #19366 use an inconsistent naming; the other similar components don't use an `Is` prefix. It also makes query filters like `Has` and `With` a bit more awkward. This is partially related to Cart's [picking concept proposal](https://gist.github.com/cart/756e48a149db2838028be600defbd24a?permalink_comment_id=5598154). ## Solution - Rename `Pointer` to `Pointer` - Rename `Pointer` to `Pointer` - Rename `Depressed` to `Pressed` - Rename `IsHovered` to `Hovered` - Rename `IsDirectlyHovered` to `DirectlyHovered` --- crates/bevy_core_widgets/src/core_button.rs | 42 ++++++------- crates/bevy_dev_tools/src/picking_debug.rs | 4 +- crates/bevy_picking/src/events.rs | 22 +++---- crates/bevy_picking/src/hover.rs | 60 +++++++++---------- crates/bevy_picking/src/lib.rs | 6 +- crates/bevy_ui/src/interaction_states.rs | 6 +- crates/bevy_ui/src/lib.rs | 6 +- examples/picking/mesh_picking.rs | 8 +-- examples/picking/sprite_picking.rs | 12 ++-- examples/ui/core_widgets.rs | 36 +++++------ examples/ui/core_widgets_observers.rs | 44 +++++++------- examples/ui/scroll.rs | 2 +- examples/usages/context_menu.rs | 8 +-- .../migration-guides/rename_pointer_events.md | 6 ++ .../release-notes/headless-widgets.md | 6 +- 15 files changed, 136 insertions(+), 132 deletions(-) create mode 100644 release-content/migration-guides/rename_pointer_events.md diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs index 23f5c28380..bf88c27143 100644 --- a/crates/bevy_core_widgets/src/core_button.rs +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -12,8 +12,8 @@ use bevy_ecs::{ }; use bevy_input::keyboard::{KeyCode, KeyboardInput}; use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; -use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Pressed, Released}; -use bevy_ui::{Depressed, InteractionDisabled}; +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` @@ -49,7 +49,7 @@ fn button_on_key_event( fn button_on_pointer_click( mut trigger: Trigger>, - mut q_state: Query<(&CoreButton, Has, Has)>, + mut q_state: Query<(&CoreButton, Has, Has)>, mut commands: Commands, ) { if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target().unwrap()) { @@ -63,17 +63,17 @@ fn button_on_pointer_click( } fn button_on_pointer_down( - mut trigger: Trigger>, - mut q_state: Query<(Entity, Has, Has), With>, + mut trigger: Trigger>, + mut q_state: Query<(Entity, Has, Has), With>, focus: Option>, focus_visible: Option>, mut commands: Commands, ) { - if let Ok((button, disabled, depressed)) = q_state.get_mut(trigger.target().unwrap()) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) { trigger.propagate(false); if !disabled { - if !depressed { - commands.entity(button).insert(Depressed); + 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. @@ -88,40 +88,40 @@ fn button_on_pointer_down( } fn button_on_pointer_up( - mut trigger: Trigger>, - mut q_state: Query<(Entity, Has, Has), With>, + mut trigger: Trigger>, + mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, depressed)) = q_state.get_mut(trigger.target().unwrap()) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) { trigger.propagate(false); - if !disabled && depressed { - commands.entity(button).remove::(); + if !disabled && pressed { + commands.entity(button).remove::(); } } } fn button_on_pointer_drag_end( mut trigger: Trigger>, - mut q_state: Query<(Entity, Has, Has), With>, + mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, depressed)) = q_state.get_mut(trigger.target().unwrap()) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) { trigger.propagate(false); - if !disabled && depressed { - commands.entity(button).remove::(); + if !disabled && pressed { + commands.entity(button).remove::(); } } } fn button_on_pointer_cancel( mut trigger: Trigger>, - mut q_state: Query<(Entity, Has, Has), With>, + mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, depressed)) = q_state.get_mut(trigger.target().unwrap()) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) { trigger.propagate(false); - if !disabled && depressed { - commands.entity(button).remove::(); + if !disabled && pressed { + commands.entity(button).remove::(); } } } diff --git a/crates/bevy_dev_tools/src/picking_debug.rs b/crates/bevy_dev_tools/src/picking_debug.rs index 79c1c8fff4..d11818dc6a 100644 --- a/crates/bevy_dev_tools/src/picking_debug.rs +++ b/crates/bevy_dev_tools/src/picking_debug.rs @@ -94,8 +94,8 @@ impl Plugin for DebugPickingPlugin { log_event_debug::.run_if(DebugPickingMode::is_noisy), log_pointer_event_debug::, log_pointer_event_debug::, - log_pointer_event_debug::, - log_pointer_event_debug::, + log_pointer_event_debug::, + log_pointer_event_debug::, log_pointer_event_debug::, log_pointer_event_trace::.run_if(DebugPickingMode::is_noisy), log_pointer_event_debug::, diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 72c0f06c46..f9083433be 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -31,7 +31,7 @@ //! //! The events this module defines fall into a few broad categories: //! + Hovering and movement: [`Over`], [`Move`], and [`Out`]. -//! + Clicking and pressing: [`Pressed`], [`Released`], and [`Click`]. +//! + Clicking and pressing: [`Press`], [`Release`], and [`Click`]. //! + Dragging and dropping: [`DragStart`], [`Drag`], [`DragEnd`], [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`]. //! //! When received by an observer, these events will always be wrapped by the [`Pointer`] type, which contains @@ -171,7 +171,7 @@ pub struct Out { /// Fires when a pointer button is pressed over the `target` entity. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] -pub struct Pressed { +pub struct Press { /// Pointer button pressed to trigger this event. pub button: PointerButton, /// Information about the picking intersection. @@ -181,7 +181,7 @@ pub struct Pressed { /// Fires when a pointer button is released over the `target` entity. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] -pub struct Released { +pub struct Release { /// Pointer button lifted to trigger this event. pub button: PointerButton, /// Information about the picking intersection. @@ -400,7 +400,7 @@ impl PointerState { pub struct PickingEventWriters<'w> { cancel_events: EventWriter<'w, Pointer>, click_events: EventWriter<'w, Pointer>, - pressed_events: EventWriter<'w, Pointer>, + pressed_events: EventWriter<'w, Pointer>, drag_drop_events: EventWriter<'w, Pointer>, drag_end_events: EventWriter<'w, Pointer>, drag_enter_events: EventWriter<'w, Pointer>, @@ -412,7 +412,7 @@ pub struct PickingEventWriters<'w> { move_events: EventWriter<'w, Pointer>, out_events: EventWriter<'w, Pointer>, over_events: EventWriter<'w, Pointer>, - released_events: EventWriter<'w, Pointer>, + released_events: EventWriter<'w, Pointer>, } /// Dispatches interaction events to the target entities. @@ -422,7 +422,7 @@ pub struct PickingEventWriters<'w> { /// + [`DragEnter`] → [`Over`]. /// + Any number of any of the following: /// + For each movement: [`DragStart`] → [`Drag`] → [`DragOver`] → [`Move`]. -/// + For each button press: [`Pressed`] or [`Click`] → [`Released`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`]. +/// + For each button press: [`Press`] or [`Click`] → [`Release`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`]. /// + For each pointer cancellation: [`Cancel`]. /// /// Additionally, across multiple frames, the following are also strictly @@ -430,7 +430,7 @@ pub struct PickingEventWriters<'w> { /// + When a pointer moves over the target: /// [`Over`], [`Move`], [`Out`]. /// + When a pointer presses buttons on the target: -/// [`Pressed`], [`Click`], [`Released`]. +/// [`Press`], [`Click`], [`Release`]. /// + When a pointer drags the target: /// [`DragStart`], [`Drag`], [`DragEnd`]. /// + When a pointer drags something over the target: @@ -452,7 +452,7 @@ pub struct PickingEventWriters<'w> { /// In the context of UI, this is especially problematic. Additional hierarchy-aware /// events will be added in a future release. /// -/// Both [`Click`] and [`Released`] target the entity hovered in the *previous frame*, +/// Both [`Click`] and [`Release`] target the entity hovered in the *previous frame*, /// rather than the current frame. This is because touch pointers hover nothing /// on the frame they are released. The end effect is that these two events can /// be received sequentially after an [`Out`] event (but always on the same frame @@ -609,7 +609,7 @@ pub fn pointer_events( pointer_id, location.clone(), hovered_entity, - Pressed { + Press { button, hit: hit.clone(), }, @@ -646,12 +646,12 @@ pub fn pointer_events( commands.trigger_targets(click_event.clone(), hovered_entity); event_writers.click_events.write(click_event); } - // Always send the Released event + // Always send the Release event let released_event = Pointer::new( pointer_id, location.clone(), hovered_entity, - Released { + Release { button, hit: hit.clone(), }, diff --git a/crates/bevy_picking/src/hover.rs b/crates/bevy_picking/src/hover.rs index 529cb94e8c..dbb6ee942e 100644 --- a/crates/bevy_picking/src/hover.rs +++ b/crates/bevy_picking/src/hover.rs @@ -296,14 +296,14 @@ fn merge_interaction_states( /// Typically, a simple hoverable entity or widget will have this component added to it. More /// complex widgets can have this component added to each hoverable part. /// -/// The computational cost of keeping the `IsHovered` components up to date is relatively cheap, and -/// linear in the number of entities that have the [`IsHovered`] component inserted. +/// The computational cost of keeping the `Hovered` components up to date is relatively cheap, and +/// linear in the number of entities that have the [`Hovered`] component inserted. #[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] #[reflect(Component, Default, PartialEq, Debug, Clone)] #[component(immutable)] -pub struct IsHovered(pub bool); +pub struct Hovered(pub bool); -impl IsHovered { +impl Hovered { /// Get whether the entity is currently hovered. pub fn get(&self) -> bool { self.0 @@ -314,24 +314,24 @@ impl IsHovered { /// is directly hovering over an entity. Users should insert this component on an entity to indicate /// interest in knowing about hover state changes. /// -/// This is similar to [`IsHovered`] component, except that it does not include descendants in the +/// This is similar to [`Hovered`] component, except that it does not include descendants in the /// hover state. #[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] #[reflect(Component, Default, PartialEq, Debug, Clone)] #[component(immutable)] -pub struct IsDirectlyHovered(pub bool); +pub struct DirectlyHovered(pub bool); -impl IsDirectlyHovered { +impl DirectlyHovered { /// Get whether the entity is currently hovered. pub fn get(&self) -> bool { self.0 } } -/// Uses [`HoverMap`] changes to update [`IsHovered`] components. +/// Uses [`HoverMap`] changes to update [`Hovered`] components. pub fn update_is_hovered( hover_map: Option>, - mut hovers: Query<(Entity, &IsHovered)>, + mut hovers: Query<(Entity, &Hovered)>, parent_query: Query<&ChildOf>, mut commands: Commands, ) { @@ -343,11 +343,11 @@ pub fn update_is_hovered( return; } - // Algorithm: for each entity having a `IsHovered` component, we want to know if the current + // Algorithm: for each entity having a `Hovered` component, we want to know if the current // entry in the hover map is "within" (that is, in the set of descenants of) that entity. Rather // than doing an expensive breadth-first traversal of children, instead start with the hovermap // entry and search upwards. We can make this even cheaper by building a set of ancestors for - // the hovermap entry, and then testing each `IsHovered` entity against that set. + // the hovermap entry, and then testing each `Hovered` entity against that set. // A set which contains the hovered for the current pointer entity and its ancestors. The // capacity is based on the likely tree depth of the hierarchy, which is typically greater for @@ -365,15 +365,15 @@ pub fn update_is_hovered( for (entity, hoverable) in hovers.iter_mut() { let is_hovering = hover_ancestors.contains(&entity); if hoverable.0 != is_hovering { - commands.entity(entity).insert(IsHovered(is_hovering)); + commands.entity(entity).insert(Hovered(is_hovering)); } } } -/// Uses [`HoverMap`] changes to update [`IsDirectlyHovered`] components. +/// Uses [`HoverMap`] changes to update [`DirectlyHovered`] components. pub fn update_is_directly_hovered( hover_map: Option>, - hovers: Query<(Entity, &IsDirectlyHovered)>, + hovers: Query<(Entity, &DirectlyHovered)>, mut commands: Commands, ) { // Don't do any work if there's no hover map. @@ -389,16 +389,14 @@ pub fn update_is_directly_hovered( for (entity, hoverable) in hovers.iter() { let is_hovering = map.contains_key(&entity); if hoverable.0 != is_hovering { - commands - .entity(entity) - .insert(IsDirectlyHovered(is_hovering)); + commands.entity(entity).insert(DirectlyHovered(is_hovering)); } } } else { // No hovered entity, reset all hovers. for (entity, hoverable) in hovers.iter() { if hoverable.0 { - commands.entity(entity).insert(IsDirectlyHovered(false)); + commands.entity(entity).insert(DirectlyHovered(false)); } } } @@ -417,7 +415,7 @@ mod tests { // Setup entities let hovered_child = world.spawn_empty().id(); - let hovered_entity = world.spawn(IsHovered(false)).add_child(hovered_child).id(); + let hovered_entity = world.spawn(Hovered(false)).add_child(hovered_child).id(); // Setup hover map with hovered_entity hovered by mouse let mut hover_map = HoverMap::default(); @@ -437,8 +435,8 @@ mod tests { // Run the system assert!(world.run_system_cached(update_is_hovered).is_ok()); - // Check to insure that the hovered entity has the IsHovered component set to true - let hover = world.entity(hovered_entity).get_ref::().unwrap(); + // Check to insure that the hovered entity has the Hovered component set to true + let hover = world.entity(hovered_entity).get_ref::().unwrap(); assert!(hover.get()); assert!(hover.is_changed()); @@ -446,7 +444,7 @@ mod tests { world.increment_change_tick(); assert!(world.run_system_cached(update_is_hovered).is_ok()); - let hover = world.entity(hovered_entity).get_ref::().unwrap(); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); assert!(hover.get()); // Should not be changed @@ -458,7 +456,7 @@ mod tests { world.increment_change_tick(); assert!(world.run_system_cached(update_is_hovered).is_ok()); - let hover = world.entity(hovered_entity).get_ref::().unwrap(); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); assert!(!hover.get()); assert!(hover.is_changed()); } @@ -469,7 +467,7 @@ mod tests { let camera = world.spawn(Camera::default()).id(); // Setup entities - let hovered_entity = world.spawn(IsDirectlyHovered(false)).id(); + let hovered_entity = world.spawn(DirectlyHovered(false)).id(); // Setup hover map with hovered_entity hovered by mouse let mut hover_map = HoverMap::default(); @@ -489,10 +487,10 @@ mod tests { // Run the system assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); - // Check to insure that the hovered entity has the IsDirectlyHovered component set to true + // Check to insure that the hovered entity has the DirectlyHovered component set to true let hover = world .entity(hovered_entity) - .get_ref::() + .get_ref::() .unwrap(); assert!(hover.get()); assert!(hover.is_changed()); @@ -503,7 +501,7 @@ mod tests { assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); let hover = world .entity(hovered_entity) - .get_ref::() + .get_ref::() .unwrap(); assert!(hover.get()); @@ -518,7 +516,7 @@ mod tests { assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); let hover = world .entity(hovered_entity) - .get_ref::() + .get_ref::() .unwrap(); assert!(!hover.get()); assert!(hover.is_changed()); @@ -532,7 +530,7 @@ mod tests { // Setup entities let hovered_child = world.spawn_empty().id(); let hovered_entity = world - .spawn(IsDirectlyHovered(false)) + .spawn(DirectlyHovered(false)) .add_child(hovered_child) .id(); @@ -554,10 +552,10 @@ mod tests { // Run the system assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); - // Check to insure that the IsDirectlyHovered component is still false + // Check to insure that the DirectlyHovered component is still false let hover = world .entity(hovered_entity) - .get_ref::() + .get_ref::() .unwrap(); assert!(!hover.get()); assert!(hover.is_changed()); diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index fde5801b42..6a7576f132 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -393,7 +393,7 @@ impl Plugin for PickingPlugin { .register_type::() .register_type::() .register_type::() - .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -416,7 +416,7 @@ impl Plugin for InteractionPlugin { .init_resource::() .add_event::>() .add_event::>() - .add_event::>() + .add_event::>() .add_event::>() .add_event::>() .add_event::>() @@ -427,7 +427,7 @@ impl Plugin for InteractionPlugin { .add_event::>() .add_event::>() .add_event::>() - .add_event::>() + .add_event::>() .add_event::>() .add_systems( PreUpdate, diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs index eed4864258..f91cdaee59 100644 --- a/crates/bevy_ui/src/interaction_states.rs +++ b/crates/bevy_ui/src/interaction_states.rs @@ -41,7 +41,7 @@ pub(crate) fn on_remove_disabled( /// Component that indicates whether a button or widget is currently in a pressed or "held down" /// state. #[derive(Component, Default, Debug)] -pub struct Depressed; +pub struct Pressed; /// Component that indicates whether a checkbox or radio button is in a checked state. #[derive(Component, Default, Debug)] @@ -55,7 +55,7 @@ impl Checked { } } -pub(crate) fn on_insert_checked(trigger: Trigger, mut world: DeferredWorld) { +pub(crate) fn on_insert_is_checked(trigger: Trigger, mut world: DeferredWorld) { let mut entity = world.entity_mut(trigger.target().unwrap()); let checked = entity.get::().unwrap().get(); if let Some(mut accessibility) = entity.get_mut::() { @@ -66,7 +66,7 @@ pub(crate) fn on_insert_checked(trigger: Trigger, mut world: } } -pub(crate) fn on_remove_checked(trigger: Trigger, mut world: DeferredWorld) { +pub(crate) fn on_remove_is_checked(trigger: Trigger, mut world: DeferredWorld) { let mut entity = world.entity_mut(trigger.target().unwrap()); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_toggled(accesskit::Toggled::False); diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 6edfad0967..ac70897d06 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -39,7 +39,7 @@ mod ui_node; pub use focus::*; pub use geometry::*; pub use gradients::*; -pub use interaction_states::{Checked, Depressed, InteractionDisabled}; +pub use interaction_states::{Checked, InteractionDisabled, Pressed}; pub use layout::*; pub use measurement::*; pub use render::*; @@ -323,8 +323,8 @@ fn build_text_interop(app: &mut App) { app.add_observer(interaction_states::on_add_disabled) .add_observer(interaction_states::on_remove_disabled) - .add_observer(interaction_states::on_insert_checked) - .add_observer(interaction_states::on_remove_checked); + .add_observer(interaction_states::on_insert_is_checked) + .add_observer(interaction_states::on_remove_is_checked); app.configure_sets( PostUpdate, diff --git a/examples/picking/mesh_picking.rs b/examples/picking/mesh_picking.rs index 4c247aa62a..c9023f48b8 100644 --- a/examples/picking/mesh_picking.rs +++ b/examples/picking/mesh_picking.rs @@ -91,8 +91,8 @@ fn setup_scene( )) .observe(update_material_on::>(hover_matl.clone())) .observe(update_material_on::>(white_matl.clone())) - .observe(update_material_on::>(pressed_matl.clone())) - .observe(update_material_on::>(hover_matl.clone())) + .observe(update_material_on::>(pressed_matl.clone())) + .observe(update_material_on::>(hover_matl.clone())) .observe(rotate_on_drag); } @@ -114,8 +114,8 @@ fn setup_scene( )) .observe(update_material_on::>(hover_matl.clone())) .observe(update_material_on::>(white_matl.clone())) - .observe(update_material_on::>(pressed_matl.clone())) - .observe(update_material_on::>(hover_matl.clone())) + .observe(update_material_on::>(pressed_matl.clone())) + .observe(update_material_on::>(hover_matl.clone())) .observe(rotate_on_drag); } diff --git a/examples/picking/sprite_picking.rs b/examples/picking/sprite_picking.rs index 99403e9192..cf7308671e 100644 --- a/examples/picking/sprite_picking.rs +++ b/examples/picking/sprite_picking.rs @@ -63,8 +63,8 @@ fn setup(mut commands: Commands, asset_server: Res) { )) .observe(recolor_on::>(Color::srgb(0.0, 1.0, 1.0))) .observe(recolor_on::>(Color::BLACK)) - .observe(recolor_on::>(Color::srgb(1.0, 1.0, 0.0))) - .observe(recolor_on::>(Color::srgb(0.0, 1.0, 1.0))); + .observe(recolor_on::>(Color::srgb(1.0, 1.0, 0.0))) + .observe(recolor_on::>(Color::srgb(0.0, 1.0, 1.0))); commands .spawn(( @@ -83,8 +83,8 @@ fn setup(mut commands: Commands, asset_server: Res) { )) .observe(recolor_on::>(Color::srgb(0.0, 1.0, 0.0))) .observe(recolor_on::>(Color::srgb(1.0, 0.0, 0.0))) - .observe(recolor_on::>(Color::srgb(0.0, 0.0, 1.0))) - .observe(recolor_on::>(Color::srgb(0.0, 1.0, 0.0))); + .observe(recolor_on::>(Color::srgb(0.0, 0.0, 1.0))) + .observe(recolor_on::>(Color::srgb(0.0, 1.0, 0.0))); } }); } @@ -145,8 +145,8 @@ fn setup_atlas( )) .observe(recolor_on::>(Color::srgb(0.0, 1.0, 1.0))) .observe(recolor_on::>(Color::srgb(1.0, 1.0, 1.0))) - .observe(recolor_on::>(Color::srgb(1.0, 1.0, 0.0))) - .observe(recolor_on::>(Color::srgb(0.0, 1.0, 1.0))); + .observe(recolor_on::>(Color::srgb(1.0, 1.0, 0.0))) + .observe(recolor_on::>(Color::srgb(0.0, 1.0, 1.0))); } // An observer listener that changes the target entity's color. diff --git a/examples/ui/core_widgets.rs b/examples/ui/core_widgets.rs index 69a45f3e50..594f43efd8 100644 --- a/examples/ui/core_widgets.rs +++ b/examples/ui/core_widgets.rs @@ -8,9 +8,9 @@ use bevy::{ tab_navigation::{TabGroup, TabIndex}, InputDispatchPlugin, }, - picking::hover::IsHovered, + picking::hover::Hovered, prelude::*, - ui::{Depressed, InteractionDisabled}, + ui::{InteractionDisabled, Pressed}, winit::WinitSettings, }; @@ -38,8 +38,8 @@ struct DemoButton; fn update_button_style( mut buttons: Query< ( - Has, - &IsHovered, + Has, + &Hovered, Has, &mut BackgroundColor, &mut BorderColor, @@ -47,8 +47,8 @@ fn update_button_style( ), ( Or<( - Changed, - Changed, + Changed, + Changed, Added, )>, With, @@ -56,12 +56,12 @@ fn update_button_style( >, mut text_query: Query<&mut Text>, ) { - for (depressed, hovered, disabled, mut color, mut border_color, children) in &mut buttons { + for (pressed, hovered, disabled, mut color, mut border_color, children) in &mut buttons { let mut text = text_query.get_mut(children[0]).unwrap(); set_button_style( disabled, hovered.get(), - depressed, + pressed, &mut color, &mut border_color, &mut text, @@ -73,8 +73,8 @@ fn update_button_style( fn update_button_style2( mut buttons: Query< ( - Has, - &IsHovered, + Has, + &Hovered, Has, &mut BackgroundColor, &mut BorderColor, @@ -82,19 +82,19 @@ fn update_button_style2( ), With, >, - mut removed_depressed: RemovedComponents, + mut removed_depressed: RemovedComponents, mut removed_disabled: RemovedComponents, mut text_query: Query<&mut Text>, ) { removed_depressed.read().for_each(|entity| { - if let Ok((depressed, hovered, disabled, mut color, mut border_color, children)) = + if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) = buttons.get_mut(entity) { let mut text = text_query.get_mut(children[0]).unwrap(); set_button_style( disabled, hovered.get(), - depressed, + pressed, &mut color, &mut border_color, &mut text, @@ -102,14 +102,14 @@ fn update_button_style2( } }); removed_disabled.read().for_each(|entity| { - if let Ok((depressed, hovered, disabled, mut color, mut border_color, children)) = + if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) = buttons.get_mut(entity) { let mut text = text_query.get_mut(children[0]).unwrap(); set_button_style( disabled, hovered.get(), - depressed, + pressed, &mut color, &mut border_color, &mut text, @@ -121,12 +121,12 @@ fn update_button_style2( fn set_button_style( disabled: bool, hovered: bool, - depressed: bool, + pressed: bool, color: &mut BackgroundColor, border_color: &mut BorderColor, text: &mut Text, ) { - match (disabled, hovered, depressed) { + match (disabled, hovered, pressed) { // Disabled button (true, _, _) => { **text = "Disabled".to_string(); @@ -192,7 +192,7 @@ fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { CoreButton { on_click: Some(on_click), }, - IsHovered::default(), + Hovered::default(), TabIndex(0), BorderColor::all(Color::BLACK), BorderRadius::MAX, diff --git a/examples/ui/core_widgets_observers.rs b/examples/ui/core_widgets_observers.rs index 55dc2d36b1..206c596e2c 100644 --- a/examples/ui/core_widgets_observers.rs +++ b/examples/ui/core_widgets_observers.rs @@ -8,9 +8,9 @@ use bevy::{ tab_navigation::{TabGroup, TabIndex}, InputDispatchPlugin, }, - picking::hover::IsHovered, + picking::hover::Hovered, prelude::*, - ui::{Depressed, InteractionDisabled}, + ui::{InteractionDisabled, Pressed}, winit::WinitSettings, }; @@ -38,10 +38,10 @@ const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); struct DemoButton; fn on_add_pressed( - trigger: Trigger, + trigger: Trigger, mut buttons: Query< ( - &IsHovered, + &Hovered, Has, &mut BackgroundColor, &mut BorderColor, @@ -67,10 +67,10 @@ fn on_add_pressed( } fn on_remove_pressed( - trigger: Trigger, + trigger: Trigger, mut buttons: Query< ( - &IsHovered, + &Hovered, Has, &mut BackgroundColor, &mut BorderColor, @@ -99,8 +99,8 @@ fn on_add_disabled( trigger: Trigger, mut buttons: Query< ( - Has, - &IsHovered, + Has, + &Hovered, &mut BackgroundColor, &mut BorderColor, &Children, @@ -109,14 +109,14 @@ fn on_add_disabled( >, mut text_query: Query<&mut Text>, ) { - if let Ok((depressed, hovered, mut color, mut border_color, children)) = + if let Ok((pressed, hovered, mut color, mut border_color, children)) = buttons.get_mut(trigger.target().unwrap()) { let mut text = text_query.get_mut(children[0]).unwrap(); set_button_style( true, hovered.get(), - depressed, + pressed, &mut color, &mut border_color, &mut text, @@ -128,8 +128,8 @@ fn on_remove_disabled( trigger: Trigger, mut buttons: Query< ( - Has, - &IsHovered, + Has, + &Hovered, &mut BackgroundColor, &mut BorderColor, &Children, @@ -138,14 +138,14 @@ fn on_remove_disabled( >, mut text_query: Query<&mut Text>, ) { - if let Ok((depressed, hovered, mut color, mut border_color, children)) = + if let Ok((pressed, hovered, mut color, mut border_color, children)) = buttons.get_mut(trigger.target().unwrap()) { let mut text = text_query.get_mut(children[0]).unwrap(); set_button_style( false, hovered.get(), - depressed, + pressed, &mut color, &mut border_color, &mut text, @@ -154,11 +154,11 @@ fn on_remove_disabled( } fn on_change_hover( - trigger: Trigger, + trigger: Trigger, mut buttons: Query< ( - Has, - &IsHovered, + Has, + &Hovered, Has, &mut BackgroundColor, &mut BorderColor, @@ -168,7 +168,7 @@ fn on_change_hover( >, mut text_query: Query<&mut Text>, ) { - if let Ok((depressed, hovered, disabled, mut color, mut border_color, children)) = + if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) = buttons.get_mut(trigger.target().unwrap()) { if children.is_empty() { @@ -180,7 +180,7 @@ fn on_change_hover( set_button_style( disabled, hovered.get(), - depressed, + pressed, &mut color, &mut border_color, &mut text, @@ -191,12 +191,12 @@ fn on_change_hover( fn set_button_style( disabled: bool, hovered: bool, - depressed: bool, + pressed: bool, color: &mut BackgroundColor, border_color: &mut BorderColor, text: &mut Text, ) { - match (disabled, hovered, depressed) { + match (disabled, hovered, pressed) { // Disabled button (true, _, _) => { **text = "Disabled".to_string(); @@ -262,7 +262,7 @@ fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { CoreButton { on_click: Some(on_click), }, - IsHovered::default(), + Hovered::default(), TabIndex(0), BorderColor::all(Color::BLACK), BorderRadius::MAX, diff --git a/examples/ui/scroll.rs b/examples/ui/scroll.rs index 535e351c16..a3a443b991 100644 --- a/examples/ui/scroll.rs +++ b/examples/ui/scroll.rs @@ -89,7 +89,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }) .observe(| - trigger: Trigger>, + trigger: Trigger>, mut commands: Commands | { if trigger.event().button == PointerButton::Primary { diff --git a/examples/usages/context_menu.rs b/examples/usages/context_menu.rs index ea595be877..d315f8badb 100644 --- a/examples/usages/context_menu.rs +++ b/examples/usages/context_menu.rs @@ -62,7 +62,7 @@ fn setup(mut commands: Commands) { commands.spawn(background_and_button()).observe( // any click bubbling up here should lead to closing any open menu - |_: Trigger>, mut commands: Commands| { + |_: Trigger>, mut commands: Commands| { commands.trigger(CloseContextMenus); }, ); @@ -108,11 +108,11 @@ fn on_trigger_menu(trigger: Trigger, mut commands: Commands) { ], )) .observe( - |trigger: Trigger>, + |trigger: Trigger>, menu_items: Query<&ContextMenuItem>, mut clear_col: ResMut, mut commands: Commands| { - // Note that we want to know the target of the `Pointer` event (Button) here. + // Note that we want to know the target of the `Pointer` event (Button) here. // Not to be confused with the trigger `target` let target = trigger.event().target; @@ -184,7 +184,7 @@ fn background_and_button() -> impl Bundle + use<> { )], )) .observe( - |mut trigger: Trigger>, mut commands: Commands| { + |mut trigger: Trigger>, mut commands: Commands| { // by default this event would bubble up further leading to the `CloseContextMenus` // event being triggered and undoing the opening of one here right away. trigger.propagate(false); diff --git a/release-content/migration-guides/rename_pointer_events.md b/release-content/migration-guides/rename_pointer_events.md new file mode 100644 index 0000000000..38e9b07821 --- /dev/null +++ b/release-content/migration-guides/rename_pointer_events.md @@ -0,0 +1,6 @@ +--- +title: Rename `Pointer` and `Pointer` to `Pointer` and `Pointer` +pull_requests: [19179] +--- + +The `Pointer` and `Pointer` events have been renamed to `Pointer` and `Pointer` for improved consistency. `Pressed` is now a marker component indicating that a button or other UI node is in a pressed or "held down" state. diff --git a/release-content/release-notes/headless-widgets.md b/release-content/release-notes/headless-widgets.md index a71e7aad2b..5d39ffe755 100644 --- a/release-content/release-notes/headless-widgets.md +++ b/release-content/release-notes/headless-widgets.md @@ -44,12 +44,12 @@ These components include: - `InteractionDisabled` - a boolean component used to indicate that a component should be "grayed out" and non-interactive. Note that these disabled widgets are still visible and can have keyboard focus (otherwise the user would have no way to discover them). -- `IsHovered` is a simple boolean component that allows detection of whether the widget is being +- `Hovered` is a simple boolean component that allows detection of whether the widget is being hovered using regular Bevy change detection. - `Checked` is a boolean component that stores the checked state of a checkbox or radio button. -- `Depressed` is used for a button-like widget, and will be true while the button is held down. +- `Pressed` is used for a button-like widget, and will be true while the button is held down. -The combination of `IsHovered` and `ButtonPressed` fulfills the same purpose as the old +The combination of `Hovered` and `Pressed` fulfills the same purpose as the old `Interaction` component, except that now we can also represent "roll-off" behavior (the state where you click on a button and then, while holding the mouse down, move the pointer out of the button's bounds). It also provides additional flexibility in cases where a widget has multiple hoverable From 12f8f9c07c9e0ce81aba72b18d9450645d51ade9 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 11 Jun 2025 15:04:52 -0700 Subject: [PATCH 005/106] Thoroughly document the current state of observers (#19590) # Objective The documentation for observers is not very good. This poses a problem to users, but *also* causes serious problems for engine devs, as they attempt to improve assorted issues surrounding observers. This PR: - Fixes #14084. - Fixes #14726. - Fixes #16538. - Closes #18914, by attempting to solve the same issue. To keep this PR at all reviewable, I've opted to simply note the various limitations (some may call them bugs!) in place, rather than attempting to fix them. There is a huge amount of cleanup work to be done here: see https://github.com/orgs/bevyengine/projects/17. ## Solution - Write good module docs for observers, offering bread crumbs to the most common methods and techniques and comparing-and-contrasting as needed. - Fix any actively misleading documentation. - Try to explain how the various bits of the (public?!) internals are related. --------- Co-authored-by: Chris Biscardi Co-authored-by: Joona Aalto --- crates/bevy_ecs/src/observer/mod.rs | 185 +++++++++++++++++++++++++--- 1 file changed, 167 insertions(+), 18 deletions(-) diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 6fcfa1621c..43beca35b4 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -1,4 +1,136 @@ -//! Types for creating and storing [`Observer`]s +//! Observers are a push-based tool for responding to [`Event`]s. +//! +//! ## Observer targeting +//! +//! Observers can be "global", listening for events that are both targeted at and not targeted at any specific entity, +//! or they can be "entity-specific", listening for events that are targeted at specific entities. +//! +//! They can also be further refined by listening to events targeted at specific components +//! (instead of using a generic event type), as is done with the [`OnAdd`] family of lifecycle events. +//! +//! When entities are observed, they will receive an [`ObservedBy`] component, +//! which will be updated to track the observers that are currently observing them. +//! +//! Currently, [observers cannot be retargeted after spawning](https://github.com/bevyengine/bevy/issues/19587): +//! despawn and respawn an observer as a workaround. +//! +//! ## Writing observers +//! +//! Observers are systems which implement [`IntoObserverSystem`] that listen for [`Event`]s matching their +//! type and target(s). +//! To write observer systems, use the [`Trigger`] system parameter as the first parameter of your system. +//! This parameter provides access to the specific event that triggered the observer, +//! as well as the entity that the event was targeted at, if any. +//! +//! Observers can request other data from the world, +//! such as via a [`Query`] or [`Res`]. Commonly, you might want to verify that +//! the entity that the observable event is targeting has a specific component, +//! or meets some other condition. +//! [`Query::get`] or [`Query::contains`] on the [`Trigger::target`] entity +//! is a good way to do this. +//! +//! [`Commands`] can also be used inside of observers. +//! This can be particularly useful for triggering other observers! +//! +//! ## Spawning observers +//! +//! Observers can be spawned via [`World::add_observer`], or the equivalent app method. +//! This will cause an entity with the [`Observer`] component to be created, +//! which will then run the observer system whenever the event it is watching is triggered. +//! +//! You can control the targets that an observer is watching by calling [`Observer::watch_entity`] +//! once the entity is spawned, or by manually spawning an entity with the [`Observer`] component +//! configured with the desired targets. +//! +//! Observers are fundamentally defined as "entities which have the [`Observer`] component" +//! allowing you to add it manually to existing entities. +//! At first, this seems convenient, but only one observer can be added to an entity at a time, +//! regardless of the event it responds to: like always, components are unique. +//! +//! Instead, a better way to achieve a similar aim is to +//! use the [`EntityWorldMut::observe`] / [`EntityCommands::observe`] method, +//! which spawns a new observer, and configures it to watch the entity it is called on. +//! Unfortunately, observers defined in this way +//! [currently cannot be spawned as part of bundles](https://github.com/bevyengine/bevy/issues/14204). +//! +//! ## Triggering observers +//! +//! Observers are most commonly triggered by [`Commands`], +//! via [`Commands::trigger`] (for untargeted events) or [`Commands::trigger_targets`] (for targeted events). +//! Like usual, equivalent methods are available on [`World`], allowing you to reduce overhead when working with exclusive world access. +//! +//! If your observer is configured to watch for a specific component or set of components instead, +//! you can pass in [`ComponentId`]s into [`Commands::trigger_targets`] by using the [`TriggerTargets`] trait. +//! As discussed in the [`Trigger`] documentation, this use case is rare, and is currently only used +//! for [lifecycle](crate::lifecycle) events, which are automatically emitted. +//! +//! ## Observer bubbling +//! +//! When events are targeted at an entity, they can optionally bubble to other targets, +//! typically up to parents in an entity hierarchy. +//! +//! This behavior is controlled via [`Event::Traversal`] and [`Event::AUTO_PROPAGATE`], +//! with the details of the propagation path specified by the [`Traversal`](crate::traversal::Traversal) trait. +//! +//! When auto-propagation is enabled, propagaion must be manually stopped to prevent the event from +//! continuing to other targets. +//! This can be done using the [`Trigger::propagate`] method on the [`Trigger`] system parameter inside of your observer. +//! +//! ## Observer timing +//! +//! Observers are triggered via [`Commands`], which imply that they are evaluated at the next sync point in the ECS schedule. +//! Accordingly, they have full access to the world, and are evaluated sequentially, in the order that the commands were sent. +//! +//! To control the relative ordering of observers sent from different systems, +//! order the systems in the schedule relative to each other. +//! +//! Currently, Bevy does not provide [a way to specify the ordering of observers](https://github.com/bevyengine/bevy/issues/14890) +//! listening to the same event relative to each other. +//! +//! Commands sent by observers are [currently not immediately applied](https://github.com/bevyengine/bevy/issues/19569). +//! Instead, all queued observers will run, and then all of the commands from those observers will be applied. +//! Careful use of [`Schedule::apply_deferred`] may help as a workaround. +//! +//! ## Lifecycle events and observers +//! +//! It is important to note that observers, just like [hooks](crate::lifecycle::ComponentHooks), +//! can listen to and respond to [lifecycle](crate::lifecycle) events. +//! Unlike hooks, observers are not treated as an "innate" part of component behavior: +//! they can be added or removed at runtime, and multiple observers +//! can be registered for the same lifecycle event for the same component. +//! +//! The ordering of hooks versus observers differs based on the lifecycle event in question: +//! +//! - when adding components, hooks are evaluated first, then observers +//! - when removing components, observers are evaluated first, then hooks +//! +//! This allows hooks to act as constructors and destructors for components, +//! as they always have the first and final say in the component's lifecycle. +//! +//! ## Cleaning up observers +//! +//! Currently, observer entities are never cleaned up, even if their target entity(s) are despawned. +//! This won't cause any runtime overhead, but is a waste of memory and can result in memory leaks. +//! +//! If you run into this problem, you could manually scan the world for observer entities and despawn them, +//! by checking if the entity in [`Observer::descriptor`] still exists. +//! +//! ## Observers vs buffered events +//! +//! By contrast, [`EventReader`] and [`EventWriter`] ("buffered events"), are pull-based. +//! They require periodically polling the world to check for new events, typically in a system that runs as part of a schedule. +//! +//! This imposes a small overhead, making observers a better choice for extremely rare events, +//! but buffered events can be more efficient for events that are expected to occur multiple times per frame, +//! as it allows for batch processing of events. +//! +//! The difference in timing is also an important consideration: +//! buffered events are evaluated at fixed points during schedules, +//! while observers are evaluated as soon as possible after the event is triggered. +//! +//! This provides more control over the timing of buffered event evaluation, +//! but allows for a more ad hoc approach with observers, +//! and enables indefinite chaining of observers triggering other observers (for both better and worse!). mod entity_observer; mod runner; @@ -29,6 +161,17 @@ use smallvec::SmallVec; /// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the /// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also /// contains event propagation information. See [`Trigger::propagate`] for more information. +/// +/// The generic `B: Bundle` is used to modify the further specialize the events that this observer is interested in. +/// The entity involved *does not* have to have these components, but the observer will only be +/// triggered if the event matches the components in `B`. +/// +/// This is used to to avoid providing a generic argument in your event, as is done for [`OnAdd`] +/// and the other lifecycle events. +/// +/// Providing multiple components in this bundle will cause this event to be triggered by any +/// matching component in the bundle, +/// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). pub struct Trigger<'w, E, B: Bundle = ()> { event: &'w mut E, propagate: &'w mut bool, @@ -69,18 +212,6 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. It may /// be [`None`] if the trigger is not for a particular entity. - /// - /// Observable events can target specific entities. When those events fire, they will trigger - /// any observers on the targeted entities. In this case, the `target()` and `observer()` are - /// the same, because the observer that was triggered is attached to the entity that was - /// targeted by the event. - /// - /// However, it is also possible for those events to bubble up the entity hierarchy and trigger - /// observers on *different* entities, or trigger a global observer. In these cases, the - /// observing entity is *different* from the entity being targeted by the event. - /// - /// This is an important distinction: the entity reacting to an event is not always the same as - /// the entity triggered by the event. pub fn target(&self) -> Option { self.trigger.target } @@ -172,10 +303,14 @@ impl<'w, E, B: Bundle> DerefMut for Trigger<'w, E, B> { } } -/// Represents a collection of targets for a specific [`Trigger`] of an [`Event`]. Targets can be of type [`Entity`] or [`ComponentId`]. +/// Represents a collection of targets for a specific [`Trigger`] of an [`Event`]. /// /// When a trigger occurs for a given event and [`TriggerTargets`], any [`Observer`] that watches for that specific event-target combination /// will run. +/// +/// This trait is implemented for both [`Entity`] and [`ComponentId`], allowing you to target specific entities or components. +/// It is also implemented for various collections of these types, such as [`Vec`], arrays, and tuples, +/// allowing you to trigger events for multiple targets at once. pub trait TriggerTargets { /// The components the trigger should target. fn components(&self) -> impl Iterator + Clone + '_; @@ -280,7 +415,9 @@ all_tuples!( T ); -/// A description of what an [`Observer`] observes. +/// Store information about what an [`Observer`] observes. +/// +/// This information is stored inside of the [`Observer`] component, #[derive(Default, Clone)] pub struct ObserverDescriptor { /// The events the observer is watching. @@ -331,7 +468,9 @@ impl ObserverDescriptor { } } -/// Event trigger metadata for a given [`Observer`], +/// Metadata about a specific [`Event`] which triggered an observer. +/// +/// This information is exposed via methods on the [`Trigger`] system parameter. #[derive(Debug)] pub struct ObserverTrigger { /// The [`Entity`] of the observer handling the trigger. @@ -357,6 +496,8 @@ impl ObserverTrigger { type ObserverMap = EntityHashMap; /// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger targeted at a specific component. +/// +/// This is stored inside of [`CachedObservers`]. #[derive(Default, Debug)] pub struct CachedComponentObservers { // Observers listening to triggers targeting this component @@ -366,6 +507,8 @@ pub struct CachedComponentObservers { } /// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger. +/// +/// This is stored inside of [`Observers`], specialized for each kind of observer. #[derive(Default, Debug)] pub struct CachedObservers { // Observers listening for any time this trigger is fired @@ -376,7 +519,13 @@ pub struct CachedObservers { entity_observers: EntityHashMap, } -/// Metadata for observers. Stores a cache mapping trigger ids to the registered observers. +/// An internal lookup table tracking all of the observers in the world. +/// +/// Stores a cache mapping trigger ids to the registered observers. +/// Some observer kinds (like [lifecycle](crate::lifecycle) observers) have a dedicated field, +/// saving lookups for the most common triggers. +/// +/// This is stored as a field of the [`World`]. #[derive(Default, Debug)] pub struct Observers { // Cached ECS observers to save a lookup most common triggers. @@ -385,7 +534,7 @@ pub struct Observers { on_replace: CachedObservers, on_remove: CachedObservers, on_despawn: CachedObservers, - // Map from trigger type to set of observers + // Map from trigger type to set of observers listening to that trigger cache: HashMap, } From 98a46f79dff002acbdd5b8577102faadfc2ca620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=9E=97=E4=BC=9F?= Date: Thu, 12 Jun 2025 06:30:35 +0800 Subject: [PATCH 006/106] Fix `bevy_core_widgets` crate description typo (#19578) # Objective Fix `bevy_core_widgets` crate description typo. --- crates/bevy_core_widgets/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml index 21540a9787..1627ff9a29 100644 --- a/crates/bevy_core_widgets/Cargo.toml +++ b/crates/bevy_core_widgets/Cargo.toml @@ -2,7 +2,7 @@ name = "bevy_core_widgets" version = "0.16.0-dev" edition = "2024" -description = "Unstyled common widgets for B Bevy Engine" +description = "Unstyled common widgets for Bevy Engine" homepage = "https://bevyengine.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" From 0d620cdf291e2bde063781cb4035b3328e5e66b9 Mon Sep 17 00:00:00 2001 From: "Al M." Date: Wed, 11 Jun 2025 15:43:40 -0700 Subject: [PATCH 007/106] Update example doc link for `ImageLoaderSettings`. (#19565) Link in the "asset settings" example. The struct was moved. --------- Co-authored-by: Alice Cecile Co-authored-by: theotherphil --- examples/asset/asset_settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/asset/asset_settings.rs b/examples/asset/asset_settings.rs index cfc76d774a..9f4c1fe507 100644 --- a/examples/asset/asset_settings.rs +++ b/examples/asset/asset_settings.rs @@ -40,7 +40,7 @@ fn setup(mut commands: Commands, asset_server: Res) { // filtering. This tends to work much better for pixel art assets. // A good reference when filling this out is to check out [ImageLoaderSettings::default()] // and follow to the default implementation of each fields type. - // https://docs.rs/bevy/latest/bevy/render/texture/struct.ImageLoaderSettings.html# + // https://docs.rs/bevy/latest/bevy/image/struct.ImageLoaderSettings.html commands.spawn(( Sprite { image: asset_server.load("bevy_pixel_dark_with_meta.png"), From 8ab71a699928895917a1fd89fdaf188bf9d739df Mon Sep 17 00:00:00 2001 From: LP Date: Wed, 11 Jun 2025 22:32:18 -0400 Subject: [PATCH 008/106] Modified the "scroll.rs" example to use the new spawning API. (#19592) # Objective - Update the scroll example to use the latest API. ## Solution - It now uses the 'children![]' API. ## Testing - I manually verified that the scrolling was working ## Limitations - Unfortunately, I couldn't find a way to spawn observers targeting the entity inside the "fn() -> impl Bundle" function. --- examples/ui/scroll.rs | 469 ++++++++++++++++++++---------------------- 1 file changed, 228 insertions(+), 241 deletions(-) diff --git a/examples/ui/scroll.rs b/examples/ui/scroll.rs index a3a443b991..34390911ee 100644 --- a/examples/ui/scroll.rs +++ b/examples/ui/scroll.rs @@ -3,6 +3,7 @@ use accesskit::{Node as Accessible, Role}; use bevy::{ a11y::AccessibilityNode, + ecs::spawn::SpawnIter, input::mouse::{MouseScrollUnit, MouseWheel}, picking::hover::HoverMap, prelude::*, @@ -26,6 +27,9 @@ fn setup(mut commands: Commands, asset_server: Res) { // Camera commands.spawn((Camera2d, IsDefaultUiCamera)); + // Font + let font_handle = asset_server.load("fonts/FiraSans-Bold.ttf"); + // root node commands .spawn(Node { @@ -49,7 +53,7 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(( Text::new("Horizontally Scrolling list (Ctrl + MouseWheel)"), TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font: font_handle.clone(), font_size: FONT_SIZE, ..default() }, @@ -71,260 +75,243 @@ fn setup(mut commands: Commands, asset_server: Res) { .with_children(|parent| { for i in 0..100 { parent.spawn((Text(format!("Item {i}")), - TextFont { - font: asset_server - .load("fonts/FiraSans-Bold.ttf"), - ..default() - }, - Label, - AccessibilityNode(Accessible::new(Role::ListItem)), + TextFont { + font: font_handle.clone(), + ..default() + }, + Label, + AccessibilityNode(Accessible::new(Role::ListItem)), )) - .insert(Node { - min_width: Val::Px(200.), - align_content: AlignContent::Center, - ..default() - }) - .insert(Pickable { - should_block_lower: false, - ..default() - }) - .observe(| - trigger: Trigger>, - mut commands: Commands - | { - if trigger.event().button == PointerButton::Primary { - commands.entity(trigger.target().unwrap()).despawn(); - } - }); + .insert(Node { + min_width: Val::Px(200.), + align_content: AlignContent::Center, + ..default() + }) + .insert(Pickable { + should_block_lower: false, + ..default() + }) + .observe(| + trigger: Trigger>, + mut commands: Commands + | { + if trigger.event().button == PointerButton::Primary { + commands.entity(trigger.target().unwrap()).despawn(); + } + }); } }); }); // container for all other examples - parent - .spawn(Node { + parent.spawn(( + Node { width: Val::Percent(100.), height: Val::Percent(100.), flex_direction: FlexDirection::Row, justify_content: JustifyContent::SpaceBetween, ..default() - }) - .with_children(|parent| { - // vertical scroll example - parent - .spawn(Node { - flex_direction: FlexDirection::Column, - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - width: Val::Px(200.), - ..default() - }) - .with_children(|parent| { - // Title - parent.spawn(( - Text::new("Vertically Scrolling List"), - TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: FONT_SIZE, - ..default() - }, - Label, - )); - // Scrolling list - parent - .spawn(( - Node { - flex_direction: FlexDirection::Column, - align_self: AlignSelf::Stretch, - height: Val::Percent(50.), - overflow: Overflow::scroll_y(), // n.b. - ..default() - }, - BackgroundColor(Color::srgb(0.10, 0.10, 0.10)), - )) - .with_children(|parent| { - // List items - for i in 0..25 { - parent - .spawn(Node { - min_height: Val::Px(LINE_HEIGHT), - max_height: Val::Px(LINE_HEIGHT), - ..default() - }) - .insert(Pickable { - should_block_lower: false, - ..default() - }) - .with_children(|parent| { - parent - .spawn(( - Text(format!("Item {i}")), - TextFont { - font: asset_server - .load("fonts/FiraSans-Bold.ttf"), - ..default() - }, - Label, - AccessibilityNode(Accessible::new( - Role::ListItem, - )), - )) - .insert(Pickable { - should_block_lower: false, - ..default() - }); - }); - } - }); - }); - - // Bidirectional scroll example - parent - .spawn(Node { - flex_direction: FlexDirection::Column, - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - width: Val::Px(200.), - ..default() - }) - .with_children(|parent| { - // Title - parent.spawn(( - Text::new("Bidirectionally Scrolling List"), - TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: FONT_SIZE, - ..default() - }, - Label, - )); - // Scrolling list - parent - .spawn(( - Node { - flex_direction: FlexDirection::Column, - align_self: AlignSelf::Stretch, - height: Val::Percent(50.), - overflow: Overflow::scroll(), // n.b. - ..default() - }, - BackgroundColor(Color::srgb(0.10, 0.10, 0.10)), - )) - .with_children(|parent| { - // Rows in each column - for oi in 0..10 { - parent - .spawn(Node { - flex_direction: FlexDirection::Row, - ..default() - }) - .insert(Pickable::IGNORE) - .with_children(|parent| { - // Elements in each row - for i in 0..25 { - parent - .spawn(( - Text(format!("Item {}", (oi * 25) + i)), - TextFont { - font: asset_server.load( - "fonts/FiraSans-Bold.ttf", - ), - ..default() - }, - Label, - AccessibilityNode(Accessible::new( - Role::ListItem, - )), - )) - .insert(Pickable { - should_block_lower: false, - ..default() - }); - } - }); - } - }); - }); - - // Nested scrolls example - parent - .spawn(Node { - flex_direction: FlexDirection::Column, - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - width: Val::Px(200.), - ..default() - }) - .with_children(|parent| { - // Title - parent.spawn(( - Text::new("Nested Scrolling Lists"), - TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: FONT_SIZE, - ..default() - }, - Label, - )); - // Outer, horizontal scrolling container - parent - .spawn(( - Node { - column_gap: Val::Px(20.), - flex_direction: FlexDirection::Row, - align_self: AlignSelf::Stretch, - height: Val::Percent(50.), - overflow: Overflow::scroll_x(), // n.b. - ..default() - }, - BackgroundColor(Color::srgb(0.10, 0.10, 0.10)), - )) - .with_children(|parent| { - // Inner, scrolling columns - for oi in 0..30 { - parent - .spawn(( - Node { - flex_direction: FlexDirection::Column, - align_self: AlignSelf::Stretch, - overflow: Overflow::scroll_y(), - ..default() - }, - BackgroundColor(Color::srgb(0.05, 0.05, 0.05)), - )) - .insert(Pickable { - should_block_lower: false, - ..default() - }) - .with_children(|parent| { - for i in 0..25 { - parent - .spawn(( - Text(format!("Item {}", (oi * 25) + i)), - TextFont { - font: asset_server.load( - "fonts/FiraSans-Bold.ttf", - ), - ..default() - }, - Label, - AccessibilityNode(Accessible::new( - Role::ListItem, - )), - )) - .insert(Pickable { - should_block_lower: false, - ..default() - }); - } - }); - } - }); - }); - }); + }, + children![ + vertically_scrolling_list(asset_server.load("fonts/FiraSans-Bold.ttf")), + bidirectional_scrolling_list(asset_server.load("fonts/FiraSans-Bold.ttf")), + nested_scrolling_list(asset_server.load("fonts/FiraSans-Bold.ttf")), + ], + )); }); } +fn vertically_scrolling_list(font_handle: Handle) -> impl Bundle { + ( + Node { + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + width: Val::Px(200.), + ..default() + }, + children![ + ( + // Title + Text::new("Vertically Scrolling List"), + TextFont { + font: font_handle.clone(), + font_size: FONT_SIZE, + ..default() + }, + Label, + ), + ( + // Scrolling list + Node { + flex_direction: FlexDirection::Column, + align_self: AlignSelf::Stretch, + height: Val::Percent(50.), + overflow: Overflow::scroll_y(), // n.b. + ..default() + }, + BackgroundColor(Color::srgb(0.10, 0.10, 0.10)), + Children::spawn(SpawnIter((0..25).map(move |i| { + ( + Node { + min_height: Val::Px(LINE_HEIGHT), + max_height: Val::Px(LINE_HEIGHT), + ..default() + }, + Pickable { + should_block_lower: false, + ..default() + }, + children![( + Text(format!("Item {i}")), + TextFont { + font: font_handle.clone(), + ..default() + }, + Label, + AccessibilityNode(Accessible::new(Role::ListItem)), + Pickable { + should_block_lower: false, + ..default() + } + )], + ) + }))) + ), + ], + ) +} + +fn bidirectional_scrolling_list(font_handle: Handle) -> impl Bundle { + ( + Node { + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + width: Val::Px(200.), + ..default() + }, + children![ + ( + Text::new("Bidirectionally Scrolling List"), + TextFont { + font: font_handle.clone(), + font_size: FONT_SIZE, + ..default() + }, + Label, + ), + ( + Node { + flex_direction: FlexDirection::Column, + align_self: AlignSelf::Stretch, + height: Val::Percent(50.), + overflow: Overflow::scroll(), // n.b. + ..default() + }, + BackgroundColor(Color::srgb(0.10, 0.10, 0.10)), + Children::spawn(SpawnIter((0..25).map(move |oi| { + ( + Node { + flex_direction: FlexDirection::Row, + ..default() + }, + Pickable::IGNORE, + Children::spawn(SpawnIter((0..10).map({ + let value = font_handle.clone(); + move |i| { + ( + Text(format!("Item {}", (oi * 10) + i)), + TextFont { + font: value.clone(), + ..default() + }, + Label, + AccessibilityNode(Accessible::new(Role::ListItem)), + Pickable { + should_block_lower: false, + ..default() + }, + ) + } + }))), + ) + }))) + ) + ], + ) +} + +fn nested_scrolling_list(font_handle: Handle) -> impl Bundle { + ( + Node { + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + width: Val::Px(200.), + ..default() + }, + children![ + ( + // Title + Text::new("Nested Scrolling Lists"), + TextFont { + font: font_handle.clone(), + font_size: FONT_SIZE, + ..default() + }, + Label, + ), + ( + // Outer, horizontal scrolling container + Node { + column_gap: Val::Px(20.), + flex_direction: FlexDirection::Row, + align_self: AlignSelf::Stretch, + height: Val::Percent(50.), + overflow: Overflow::scroll_x(), // n.b. + ..default() + }, + BackgroundColor(Color::srgb(0.10, 0.10, 0.10)), + // Inner, scrolling columns + Children::spawn(SpawnIter((0..30).map(move |oi| { + ( + Node { + flex_direction: FlexDirection::Column, + align_self: AlignSelf::Stretch, + overflow: Overflow::scroll_y(), + ..default() + }, + BackgroundColor(Color::srgb(0.05, 0.05, 0.05)), + Pickable { + should_block_lower: false, + ..default() + }, + Children::spawn(SpawnIter((0..30).map({ + let value = font_handle.clone(); + move |i| { + ( + Text(format!("Item {}", (oi * 25) + i)), + TextFont { + font: value.clone(), + ..default() + }, + Label, + AccessibilityNode(Accessible::new(Role::ListItem)), + Pickable { + should_block_lower: false, + ..default() + }, + ) + } + }))), + ) + }))) + ) + ], + ) +} + /// Updates the scroll position of scrollable nodes in response to mouse input pub fn update_scroll_position( mut mouse_wheel_events: EventReader, From e5dc177b4b0a42671d73fe65f2a377db5eed5bc1 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Thu, 12 Jun 2025 21:22:33 +0300 Subject: [PATCH 009/106] Rename `Trigger` to `On` (#19596) # Objective Currently, the observer API looks like this: ```rust app.add_observer(|trigger: Trigger| { info!("Entity {} exploded!", trigger.target()); }); ``` Future plans for observers also include "multi-event observers" with a trigger that looks like this (see [Cart's example](https://github.com/bevyengine/bevy/issues/14649#issuecomment-2960402508)): ```rust trigger: Trigger<( OnAdd, OnRemove, OnAdd, OnRemove, OnInsert, )>, ``` In scenarios like this, there is a lot of repetition of `On`. These are expected to be very high-traffic APIs especially in UI contexts, so ergonomics and readability are critical. By renaming `Trigger` to `On`, we can make these APIs read more cleanly and get rid of the repetition: ```rust app.add_observer(|trigger: On| { info!("Entity {} exploded!", trigger.target()); }); ``` ```rust trigger: On<( Add, Remove, Add, Remove, Insert, )>, ``` Names like `On>` emphasize the actual event listener nature more than `Trigger>`, and look cleaner. This *also* frees up the `Trigger` name if we want to use it for the observer event type, splitting them out from buffered events (bikeshedding this is out of scope for this PR though). For prior art: [`bevy_eventlistener`](https://github.com/aevyrie/bevy_eventlistener) used [`On`](https://docs.rs/bevy_eventlistener/latest/bevy_eventlistener/event_listener/struct.On.html) for its event listener type. Though in our case, the observer is the event listener, and `On` is just a type containing information about the triggered event. ## Solution Steal from `bevy_event_listener` by @aevyrie and use `On`. - Rename `Trigger` to `On` - Rename `OnAdd` to `Add` - Rename `OnInsert` to `Insert` - Rename `OnReplace` to `Replace` - Rename `OnRemove` to `Remove` - Rename `OnDespawn` to `Despawn` ## Discussion ### Naming Conflicts?? Using a name like `Add` might initially feel like a very bad idea, since it risks conflict with `core::ops::Add`. However, I don't expect this to be a big problem in practice. - You rarely need to actually implement the `Add` trait, especially in modules that would use the Bevy ECS. - In the rare cases where you *do* get a conflict, it is very easy to fix by just disambiguating, for example using `ops::Add`. - The `Add` event is a struct while the `Add` trait is a trait (duh), so the compiler error should be very obvious. For the record, renaming `OnAdd` to `Add`, I got exactly *zero* errors or conflicts within Bevy itself. But this is of course not entirely representative of actual projects *using* Bevy. You might then wonder, why not use `Added`? This would conflict with the `Added` query filter, so it wouldn't work. Additionally, the current naming convention for observer events does not use past tense. ### Documentation This does make documentation slightly more awkward when referring to `On` or its methods. Previous docs often referred to `Trigger::target` or "sends a `Trigger`" (which is... a bit strange anyway), which would now be `On::target` and "sends an observer `Event`". You can see the diff in this PR to see some of the effects. I think it should be fine though, we may just need to reword more documentation to read better. --- .../benches/bevy_ecs/observers/propagation.rs | 2 +- benches/benches/bevy_ecs/observers/simple.rs | 4 +- crates/bevy_app/src/app.rs | 4 +- crates/bevy_core_widgets/src/core_button.rs | 14 +- crates/bevy_ecs/README.md | 4 +- crates/bevy_ecs/src/archetype.rs | 20 +- crates/bevy_ecs/src/bundle.rs | 20 +- crates/bevy_ecs/src/component.rs | 4 +- crates/bevy_ecs/src/event/base.rs | 9 +- crates/bevy_ecs/src/lib.rs | 11 +- crates/bevy_ecs/src/lifecycle.rs | 81 +++-- .../bevy_ecs/src/observer/entity_observer.rs | 4 +- crates/bevy_ecs/src/observer/mod.rs | 296 ++++++++---------- crates/bevy_ecs/src/observer/runner.rs | 35 +-- .../bevy_ecs/src/system/commands/command.rs | 8 +- .../src/system/commands/entity_command.rs | 2 +- crates/bevy_ecs/src/system/commands/mod.rs | 10 +- crates/bevy_ecs/src/system/input.rs | 10 +- crates/bevy_ecs/src/system/mod.rs | 10 +- crates/bevy_ecs/src/system/observer_system.rs | 24 +- crates/bevy_ecs/src/traversal.rs | 6 +- crates/bevy_ecs/src/world/deferred_world.rs | 18 +- crates/bevy_ecs/src/world/entity_ref.rs | 54 ++-- crates/bevy_ecs/src/world/mod.rs | 28 +- crates/bevy_input_focus/src/lib.rs | 4 +- crates/bevy_input_focus/src/tab_navigation.rs | 4 +- crates/bevy_pbr/src/render/light.rs | 6 +- crates/bevy_picking/src/events.rs | 2 +- crates/bevy_picking/src/lib.rs | 14 +- crates/bevy_render/src/sync_world.rs | 16 +- .../bevy_render/src/view/window/screenshot.rs | 2 +- crates/bevy_scene/src/scene_spawner.rs | 8 +- crates/bevy_ui/src/interaction_states.rs | 15 +- crates/bevy_winit/src/cursor.rs | 6 +- examples/3d/edit_material_on_gltf.rs | 4 +- examples/3d/mixed_lighting.rs | 2 +- examples/animation/animated_mesh.rs | 2 +- examples/animation/animated_mesh_events.rs | 2 +- examples/animation/animation_events.rs | 2 +- examples/ecs/entity_disabling.rs | 2 +- examples/ecs/error_handling.rs | 2 +- examples/ecs/hotpatching_systems.rs | 2 +- examples/ecs/immutable_components.rs | 4 +- examples/ecs/observer_propagation.rs | 8 +- examples/ecs/observers.rs | 16 +- examples/ecs/removal_detection.rs | 6 +- examples/no_std/library/src/lib.rs | 2 +- examples/picking/debug_picking.rs | 8 +- examples/picking/mesh_picking.rs | 4 +- examples/picking/simple_picking.rs | 16 +- examples/picking/sprite_picking.rs | 4 +- examples/shader/gpu_readback.rs | 16 +- examples/testbed/3d.rs | 2 +- examples/ui/core_widgets_observers.rs | 10 +- examples/ui/directional_navigation.rs | 2 +- examples/ui/scroll.rs | 35 ++- examples/ui/tab_navigation.rs | 4 +- examples/ui/viewport_node.rs | 4 +- examples/usages/context_menu.rs | 32 +- .../component-lifecycle-module.md | 4 +- .../migration-guides/observer_triggers.md | 28 +- .../release-notes/observer_overhaul.md | 34 ++ 62 files changed, 532 insertions(+), 480 deletions(-) create mode 100644 release-content/release-notes/observer_overhaul.md diff --git a/benches/benches/bevy_ecs/observers/propagation.rs b/benches/benches/bevy_ecs/observers/propagation.rs index 65c15f7308..e2be1afed4 100644 --- a/benches/benches/bevy_ecs/observers/propagation.rs +++ b/benches/benches/bevy_ecs/observers/propagation.rs @@ -117,6 +117,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..b3006ec924 100644 --- a/benches/benches/bevy_ecs/observers/simple.rs +++ b/benches/benches/bevy_ecs/observers/simple.rs @@ -2,7 +2,7 @@ use core::hint::black_box; use bevy_ecs::{ event::Event, - observer::{Trigger, TriggerTargets}, + observer::{On, TriggerTargets}, world::World, }; @@ -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_app/src/app.rs b/crates/bevy_app/src/app.rs index 2adf6c2857..a9d17c7a0b 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -1306,7 +1306,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 /// @@ -1329,7 +1329,7 @@ impl App { /// # 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); diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs index bf88c27143..31afbc1aca 100644 --- a/crates/bevy_core_widgets/src/core_button.rs +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -6,7 +6,7 @@ use bevy_ecs::system::ResMut; use bevy_ecs::{ component::Component, entity::Entity, - observer::Trigger, + observer::On, query::With, system::{Commands, Query, SystemId}, }; @@ -28,7 +28,7 @@ pub struct CoreButton { } fn button_on_key_event( - mut trigger: Trigger>, + mut trigger: On>, q_state: Query<(&CoreButton, Has)>, mut commands: Commands, ) { @@ -48,7 +48,7 @@ fn button_on_key_event( } fn button_on_pointer_click( - mut trigger: Trigger>, + mut trigger: On>, mut q_state: Query<(&CoreButton, Has, Has)>, mut commands: Commands, ) { @@ -63,7 +63,7 @@ fn button_on_pointer_click( } fn button_on_pointer_down( - mut trigger: Trigger>, + mut trigger: On>, mut q_state: Query<(Entity, Has, Has), With>, focus: Option>, focus_visible: Option>, @@ -88,7 +88,7 @@ fn button_on_pointer_down( } fn button_on_pointer_up( - mut trigger: Trigger>, + mut trigger: On>, mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { @@ -101,7 +101,7 @@ fn button_on_pointer_up( } fn button_on_pointer_drag_end( - mut trigger: Trigger>, + mut trigger: On>, mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { @@ -114,7 +114,7 @@ fn button_on_pointer_drag_end( } fn button_on_pointer_cancel( - mut trigger: Trigger>, + mut trigger: On>, mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index ade3866b5d..6ab8d45b7f 100644 --- a/crates/bevy_ecs/README.md +++ b/crates/bevy_ecs/README.md @@ -315,7 +315,7 @@ struct MyEvent { let mut world = World::new(); -world.add_observer(|trigger: Trigger| { +world.add_observer(|trigger: On| { println!("{}", trigger.event().message); }); @@ -339,7 +339,7 @@ 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().unwrap()); commands.entity(trigger.target().unwrap()).despawn(); }); diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 7369028715..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::lifecycle::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::lifecycle::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::lifecycle::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::lifecycle::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::lifecycle::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 7d6c0129dc..a264f0b14b 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -66,7 +66,7 @@ use crate::{ RequiredComponents, StorageType, Tick, }, entity::{Entities, Entity, EntityLocation}, - lifecycle::{ON_ADD, ON_INSERT, ON_REMOVE, ON_REPLACE}, + lifecycle::{ADD, INSERT, REMOVE, REPLACE}, observer::Observers, prelude::World, query::DebugCheckedUnwrap, @@ -1191,7 +1191,7 @@ impl<'w> BundleInserter<'w> { if insert_mode == InsertMode::Replace { if archetype.has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, + REPLACE, Some(entity), archetype_after_insert.iter_existing(), caller, @@ -1376,7 +1376,7 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_add_observer() { deferred_world.trigger_observers( - ON_ADD, + ADD, Some(entity), archetype_after_insert.iter_added(), caller, @@ -1394,7 +1394,7 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, + INSERT, Some(entity), archetype_after_insert.iter_inserted(), caller, @@ -1413,7 +1413,7 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, + INSERT, Some(entity), archetype_after_insert.iter_added(), caller, @@ -1567,7 +1567,7 @@ impl<'w> BundleRemover<'w> { }; if self.old_archetype.as_ref().has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, + REPLACE, Some(entity), bundle_components_in_archetype(), caller, @@ -1582,7 +1582,7 @@ impl<'w> BundleRemover<'w> { ); if self.old_archetype.as_ref().has_remove_observer() { deferred_world.trigger_observers( - ON_REMOVE, + REMOVE, Some(entity), bundle_components_in_archetype(), caller, @@ -1833,7 +1833,7 @@ impl<'w> BundleSpawner<'w> { ); if archetype.has_add_observer() { deferred_world.trigger_observers( - ON_ADD, + ADD, Some(entity), bundle_info.iter_contributed_components(), caller, @@ -1848,7 +1848,7 @@ impl<'w> BundleSpawner<'w> { ); if archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, + INSERT, Some(entity), bundle_info.iter_contributed_components(), caller, @@ -2386,7 +2386,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; }); diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 338050f5fb..4ebf1decd9 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -598,7 +598,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. @@ -2415,7 +2415,7 @@ impl Tick { /// struct CustomSchedule(Schedule); /// /// # let mut world = World::new(); -/// world.add_observer(|tick: Trigger, mut schedule: ResMut| { +/// world.add_observer(|tick: On, mut schedule: ResMut| { /// schedule.0.check_change_ticks(tick.get()); /// }); /// ``` diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index bb896c4f09..75e86f1b03 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -46,16 +46,17 @@ use core::{ note = "consider annotating `{Self}` with `#[derive(Event)]`" )] pub trait Event: Send + Sync + 'static { - /// The component that describes which Entity to propagate this event to next, when [propagation] is enabled. + /// The component that describes which [`Entity`] to propagate this event to next, when [propagation] is enabled. /// - /// [propagation]: crate::observer::Trigger::propagate + /// [`Entity`]: crate::entity::Entity + /// [propagation]: crate::observer::On::propagate type Traversal: Traversal; /// When true, this event will always attempt to propagate when [triggered], without requiring a call - /// to [`Trigger::propagate`]. + /// to [`On::propagate`]. /// /// [triggered]: crate::system::Commands::trigger_targets - /// [`Trigger::propagate`]: crate::observer::Trigger::propagate + /// [`On::propagate`]: crate::observer::On::propagate const AUTO_PROPAGATE: bool = false; /// Generates the [`ComponentId`] for this event type. diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 7c10127fcc..d8f88ed4a3 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -67,6 +67,10 @@ use event::Event; /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] + #[expect( + deprecated, + reason = "`Trigger` was deprecated in favor of `On`, and `OnX` lifecycle events were deprecated in favor of `X` events." + )] pub use crate::{ bundle::Bundle, change_detection::{DetectChanges, DetectChangesMut, Mut, Ref}, @@ -76,9 +80,12 @@ pub mod prelude { error::{BevyError, Result}, event::{Event, EventMutator, EventReader, EventWriter, Events}, hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children}, - lifecycle::{OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace, RemovedComponents}, + lifecycle::{ + Add, Despawn, Insert, OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace, Remove, + RemovedComponents, Replace, + }, name::{Name, NameOrEntity}, - observer::{Observer, Trigger}, + observer::{Observer, On, Trigger}, query::{Added, Allows, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, related, relationship::RelationshipTarget, diff --git a/crates/bevy_ecs/src/lifecycle.rs b/crates/bevy_ecs/src/lifecycle.rs index e228b416c3..f9707bae85 100644 --- a/crates/bevy_ecs/src/lifecycle.rs +++ b/crates/bevy_ecs/src/lifecycle.rs @@ -16,26 +16,26 @@ //! There are five types of lifecycle events, split into two categories. First, we have lifecycle events that are triggered //! when a component is added to an entity: //! -//! - [`OnAdd`]: Triggered when a component is added to an entity that did not already have it. -//! - [`OnInsert`]: Triggered when a component is added to an entity, regardless of whether it already had it. +//! - [`Add`]: Triggered when a component is added to an entity that did not already have it. +//! - [`Insert`]: Triggered when a component is added to an entity, regardless of whether it already had it. //! -//! When both events occur, [`OnAdd`] hooks are evaluated before [`OnInsert`]. +//! When both events occur, [`Add`] hooks are evaluated before [`Insert`]. //! //! Next, we have lifecycle events that are triggered when a component is removed from an entity: //! -//! - [`OnReplace`]: Triggered when a component is removed from an entity, regardless if it is then replaced with a new value. -//! - [`OnRemove`]: Triggered when a component is removed from an entity and not replaced, before the component is removed. -//! - [`OnDespawn`]: Triggered for each component on an entity when it is despawned. +//! - [`Replace`]: Triggered when a component is removed from an entity, regardless if it is then replaced with a new value. +//! - [`Remove`]: Triggered when a component is removed from an entity and not replaced, before the component is removed. +//! - [`Despawn`]: Triggered for each component on an entity when it is despawned. //! -//! [`OnReplace`] hooks are evaluated before [`OnRemove`], then finally [`OnDespawn`] hooks are evaluated. +//! [`Replace`] hooks are evaluated before [`Remove`], then finally [`Despawn`] hooks are evaluated. //! -//! [`OnAdd`] and [`OnRemove`] are counterparts: they are only triggered when a component is added or removed +//! [`Add`] and [`Remove`] are counterparts: they are only triggered when a component is added or removed //! from an entity in such a way as to cause a change in the component's presence on that entity. -//! Similarly, [`OnInsert`] and [`OnReplace`] are counterparts: they are triggered when a component is added or replaced +//! Similarly, [`Insert`] and [`Replace`] are counterparts: they are triggered when a component is added or replaced //! on an entity, regardless of whether this results in a change in the component's presence on that entity. //! //! To reliably synchronize data structures using with component lifecycle events, -//! you can combine [`OnInsert`] and [`OnReplace`] to fully capture any changes to the data. +//! you can combine [`Insert`] and [`Replace`] to fully capture any changes to the data. //! This is particularly useful in combination with immutable components, //! to avoid any lifecycle-bypassing mutations. //! @@ -47,7 +47,7 @@ //! //! Each of these lifecycle events also corresponds to a fixed [`ComponentId`], //! which are assigned during [`World`] initialization. -//! For example, [`OnAdd`] corresponds to [`ON_ADD`]. +//! For example, [`Add`] corresponds to [`ADD`]. //! This is used to skip [`TypeId`](core::any::TypeId) lookups in hot paths. use crate::{ change_detection::MaybeLocation, @@ -310,32 +310,34 @@ impl ComponentHooks { } } -/// [`ComponentId`] for [`OnAdd`] -pub const ON_ADD: ComponentId = ComponentId::new(0); -/// [`ComponentId`] for [`OnInsert`] -pub const ON_INSERT: ComponentId = ComponentId::new(1); -/// [`ComponentId`] for [`OnReplace`] -pub const ON_REPLACE: ComponentId = ComponentId::new(2); -/// [`ComponentId`] for [`OnRemove`] -pub const ON_REMOVE: ComponentId = ComponentId::new(3); -/// [`ComponentId`] for [`OnDespawn`] -pub const ON_DESPAWN: ComponentId = ComponentId::new(4); +/// [`ComponentId`] for [`Add`] +pub const ADD: ComponentId = ComponentId::new(0); +/// [`ComponentId`] for [`Insert`] +pub const INSERT: ComponentId = ComponentId::new(1); +/// [`ComponentId`] for [`Replace`] +pub const REPLACE: ComponentId = ComponentId::new(2); +/// [`ComponentId`] for [`Remove`] +pub const REMOVE: ComponentId = ComponentId::new(3); +/// [`ComponentId`] for [`Despawn`] +pub const DESPAWN: ComponentId = ComponentId::new(4); /// Trigger emitted when a component is inserted onto an entity that does not already have that -/// component. Runs before `OnInsert`. +/// component. Runs before `Insert`. /// See [`crate::lifecycle::ComponentHooks::on_add`] for more information. #[derive(Event, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnAdd; +#[doc(alias = "OnAdd")] +pub struct Add; /// Trigger emitted when a component is inserted, regardless of whether or not the entity already -/// had that component. Runs after `OnAdd`, if it ran. +/// had that component. Runs after `Add`, if it ran. /// See [`crate::lifecycle::ComponentHooks::on_insert`] for more information. #[derive(Event, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnInsert; +#[doc(alias = "OnInsert")] +pub struct Insert; /// Trigger emitted when a component is removed from an entity, regardless /// of whether or not it is later replaced. @@ -345,7 +347,8 @@ pub struct OnInsert; #[derive(Event, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnReplace; +#[doc(alias = "OnReplace")] +pub struct Replace; /// Trigger emitted when a component is removed from an entity, and runs before the component is /// removed, so you can still access the component data. @@ -353,14 +356,36 @@ pub struct OnReplace; #[derive(Event, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnRemove; +#[doc(alias = "OnRemove")] +pub struct Remove; /// Trigger emitted for each component on an entity when it is despawned. /// See [`crate::lifecycle::ComponentHooks::on_despawn`] for more information. #[derive(Event, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnDespawn; +#[doc(alias = "OnDespawn")] +pub struct Despawn; + +/// Deprecated in favor of [`Add`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Add`.")] +pub type OnAdd = Add; + +/// Deprecated in favor of [`Insert`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Insert`.")] +pub type OnInsert = Insert; + +/// Deprecated in favor of [`Replace`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Replace`.")] +pub type OnReplace = Replace; + +/// Deprecated in favor of [`Remove`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Remove`.")] +pub type OnRemove = Remove; + +/// Deprecated in favor of [`Despawn`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Despawn`.")] +pub type OnDespawn = Despawn; /// Wrapper around [`Entity`] for [`RemovedComponents`]. /// Internally, `RemovedComponents` uses these as an `Events`. diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_observer.rs index bd45072a5a..74790a88b2 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_observer.rs @@ -109,7 +109,7 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo #[cfg(test)] mod tests { use crate::{ - entity::EntityCloner, event::Event, observer::Trigger, resource::Resource, system::ResMut, + entity::EntityCloner, event::Event, observer::On, resource::Resource, system::ResMut, world::World, }; @@ -126,7 +126,7 @@ mod tests { let e = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .observe(|_: On, mut res: ResMut| res.0 += 1) .id(); world.flush(); diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 43beca35b4..0d0ca66299 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -6,7 +6,7 @@ //! or they can be "entity-specific", listening for events that are targeted at specific entities. //! //! They can also be further refined by listening to events targeted at specific components -//! (instead of using a generic event type), as is done with the [`OnAdd`] family of lifecycle events. +//! (instead of using a generic event type), as is done with the [`Add`] family of lifecycle events. //! //! When entities are observed, they will receive an [`ObservedBy`] component, //! which will be updated to track the observers that are currently observing them. @@ -18,16 +18,14 @@ //! //! Observers are systems which implement [`IntoObserverSystem`] that listen for [`Event`]s matching their //! type and target(s). -//! To write observer systems, use the [`Trigger`] system parameter as the first parameter of your system. +//! To write observer systems, use [`On`] as the first parameter of your system. //! This parameter provides access to the specific event that triggered the observer, //! as well as the entity that the event was targeted at, if any. //! -//! Observers can request other data from the world, -//! such as via a [`Query`] or [`Res`]. Commonly, you might want to verify that -//! the entity that the observable event is targeting has a specific component, -//! or meets some other condition. -//! [`Query::get`] or [`Query::contains`] on the [`Trigger::target`] entity -//! is a good way to do this. +//! Observers can request other data from the world, such as via a [`Query`] or [`Res`]. +//! Commonly, you might want to verify that the entity that the observable event is targeting +//! has a specific component, or meets some other condition. [`Query::get`] or [`Query::contains`] +//! on the [`On::target`] entity is a good way to do this. //! //! [`Commands`] can also be used inside of observers. //! This can be particularly useful for triggering other observers! @@ -61,7 +59,7 @@ //! //! If your observer is configured to watch for a specific component or set of components instead, //! you can pass in [`ComponentId`]s into [`Commands::trigger_targets`] by using the [`TriggerTargets`] trait. -//! As discussed in the [`Trigger`] documentation, this use case is rare, and is currently only used +//! As discussed in the [`On`] documentation, this use case is rare, and is currently only used //! for [lifecycle](crate::lifecycle) events, which are automatically emitted. //! //! ## Observer bubbling @@ -74,9 +72,9 @@ //! //! When auto-propagation is enabled, propagaion must be manually stopped to prevent the event from //! continuing to other targets. -//! This can be done using the [`Trigger::propagate`] method on the [`Trigger`] system parameter inside of your observer. +//! This can be done using the [`On::propagate`] method inside of your observer. //! -//! ## Observer timing +//! ## Observer timing //! //! Observers are triggered via [`Commands`], which imply that they are evaluated at the next sync point in the ECS schedule. //! Accordingly, they have full access to the world, and are evaluated sequentially, in the order that the commands were sent. @@ -160,27 +158,31 @@ use smallvec::SmallVec; /// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the /// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also -/// contains event propagation information. See [`Trigger::propagate`] for more information. +/// contains event propagation information. See [`On::propagate`] for more information. /// /// The generic `B: Bundle` is used to modify the further specialize the events that this observer is interested in. /// The entity involved *does not* have to have these components, but the observer will only be /// triggered if the event matches the components in `B`. /// -/// This is used to to avoid providing a generic argument in your event, as is done for [`OnAdd`] +/// This is used to to avoid providing a generic argument in your event, as is done for [`Add`] /// and the other lifecycle events. /// /// Providing multiple components in this bundle will cause this event to be triggered by any /// matching component in the bundle, /// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). -pub struct Trigger<'w, E, B: Bundle = ()> { +pub struct On<'w, E, B: Bundle = ()> { event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger, _marker: PhantomData, } -impl<'w, E, B: Bundle> Trigger<'w, E, B> { - /// Creates a new trigger for the given event and observer information. +/// Deprecated in favor of [`On`]. +#[deprecated(since = "0.17.0", note = "Renamed to `On`.")] +pub type Trigger<'w, E, B = ()> = On<'w, E, B>; + +impl<'w, E, B: Bundle> On<'w, E, B> { + /// Creates a new instance of [`On`] for the given event and observer information. pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self { Self { event, @@ -190,7 +192,7 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { } } - /// Returns the event type of this trigger. + /// Returns the event type of this [`On`] instance. pub fn event_type(&self) -> ComponentId { self.trigger.event_type } @@ -229,14 +231,14 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { /// # Examples /// /// ```rust - /// # use bevy_ecs::prelude::{Commands, Trigger}; + /// # use bevy_ecs::prelude::{Commands, On}; /// # /// # struct MyEvent { /// # done: bool, /// # } /// # /// /// Handle `MyEvent` and if it is done, stop observation. - /// fn my_observer(trigger: Trigger, mut commands: Commands) { + /// fn my_observer(trigger: On, mut commands: Commands) { /// if trigger.event().done { /// commands.entity(trigger.observer()).despawn(); /// return; @@ -267,7 +269,7 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { /// Returns the value of the flag that controls event propagation. See [`propagate`] for more information. /// - /// [`propagate`]: Trigger::propagate + /// [`propagate`]: On::propagate pub fn get_propagate(&self) -> bool { *self.propagate } @@ -278,9 +280,9 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { } } -impl<'w, E: Debug, B: Bundle> Debug for Trigger<'w, E, B> { +impl<'w, E: Debug, B: Bundle> Debug for On<'w, E, B> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("Trigger") + f.debug_struct("On") .field("event", &self.event) .field("propagate", &self.propagate) .field("trigger", &self.trigger) @@ -289,7 +291,7 @@ impl<'w, E: Debug, B: Bundle> Debug for Trigger<'w, E, B> { } } -impl<'w, E, B: Bundle> Deref for Trigger<'w, E, B> { +impl<'w, E, B: Bundle> Deref for On<'w, E, B> { type Target = E; fn deref(&self) -> &Self::Target { @@ -297,16 +299,16 @@ impl<'w, E, B: Bundle> Deref for Trigger<'w, E, B> { } } -impl<'w, E, B: Bundle> DerefMut for Trigger<'w, E, B> { +impl<'w, E, B: Bundle> DerefMut for On<'w, E, B> { fn deref_mut(&mut self) -> &mut Self::Target { self.event } } -/// Represents a collection of targets for a specific [`Trigger`] of an [`Event`]. +/// Represents a collection of targets for a specific [`On`] instance of an [`Event`]. /// -/// When a trigger occurs for a given event and [`TriggerTargets`], any [`Observer`] that watches for that specific event-target combination -/// will run. +/// When an event is triggered with [`TriggerTargets`], any [`Observer`] that watches for that specific +/// event-target combination will run. /// /// This trait is implemented for both [`Entity`] and [`ComponentId`], allowing you to target specific entities or components. /// It is also implemented for various collections of these types, such as [`Vec`], arrays, and tuples, @@ -468,9 +470,9 @@ impl ObserverDescriptor { } } -/// Metadata about a specific [`Event`] which triggered an observer. +/// Metadata about a specific [`Event`] that triggered an observer. /// -/// This information is exposed via methods on the [`Trigger`] system parameter. +/// This information is exposed via methods on [`On`]. #[derive(Debug)] pub struct ObserverTrigger { /// The [`Entity`] of the observer handling the trigger. @@ -529,11 +531,11 @@ pub struct CachedObservers { #[derive(Default, Debug)] pub struct Observers { // Cached ECS observers to save a lookup most common triggers. - on_add: CachedObservers, - on_insert: CachedObservers, - on_replace: CachedObservers, - on_remove: CachedObservers, - on_despawn: CachedObservers, + add: CachedObservers, + insert: CachedObservers, + replace: CachedObservers, + remove: CachedObservers, + despawn: CachedObservers, // Map from trigger type to set of observers listening to that trigger cache: HashMap, } @@ -543,11 +545,11 @@ impl Observers { use crate::lifecycle::*; match event_type { - ON_ADD => &mut self.on_add, - ON_INSERT => &mut self.on_insert, - ON_REPLACE => &mut self.on_replace, - ON_REMOVE => &mut self.on_remove, - ON_DESPAWN => &mut self.on_despawn, + ADD => &mut self.add, + INSERT => &mut self.insert, + REPLACE => &mut self.replace, + REMOVE => &mut self.remove, + DESPAWN => &mut self.despawn, _ => self.cache.entry(event_type).or_default(), } } @@ -556,11 +558,11 @@ impl Observers { use crate::lifecycle::*; match event_type { - ON_ADD => Some(&self.on_add), - ON_INSERT => Some(&self.on_insert), - ON_REPLACE => Some(&self.on_replace), - ON_REMOVE => Some(&self.on_remove), - ON_DESPAWN => Some(&self.on_despawn), + ADD => Some(&self.add), + INSERT => Some(&self.insert), + REPLACE => Some(&self.replace), + REMOVE => Some(&self.remove), + DESPAWN => Some(&self.despawn), _ => self.cache.get(&event_type), } } @@ -635,11 +637,11 @@ impl Observers { use crate::lifecycle::*; match event_type { - ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), - ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), - ON_REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER), - ON_REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), - ON_DESPAWN => Some(ArchetypeFlags::ON_DESPAWN_OBSERVER), + ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), + INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), + REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER), + REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), + DESPAWN => Some(ArchetypeFlags::ON_DESPAWN_OBSERVER), _ => None, } } @@ -649,39 +651,23 @@ impl Observers { component_id: ComponentId, flags: &mut ArchetypeFlags, ) { - if self.on_add.component_observers.contains_key(&component_id) { + if self.add.component_observers.contains_key(&component_id) { flags.insert(ArchetypeFlags::ON_ADD_OBSERVER); } - if self - .on_insert - .component_observers - .contains_key(&component_id) - { + if self.insert.component_observers.contains_key(&component_id) { flags.insert(ArchetypeFlags::ON_INSERT_OBSERVER); } - if self - .on_replace - .component_observers - .contains_key(&component_id) - { + if self.replace.component_observers.contains_key(&component_id) { flags.insert(ArchetypeFlags::ON_REPLACE_OBSERVER); } - if self - .on_remove - .component_observers - .contains_key(&component_id) - { + if self.remove.component_observers.contains_key(&component_id) { flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER); } - if self - .on_despawn - .component_observers - .contains_key(&component_id) - { + if self.despawn.component_observers.contains_key(&component_id) { flags.insert(ArchetypeFlags::ON_DESPAWN_OBSERVER); } } @@ -691,7 +677,7 @@ impl World { /// Spawns a "global" [`Observer`] which will watch for the given event. /// Returns its [`Entity`] as a [`EntityWorldMut`]. /// - /// `system` can be any system whose first parameter is a [`Trigger`]. + /// `system` can be any system whose first parameter is [`On`]. /// /// **Calling [`observe`](EntityWorldMut::observe) on the returned /// [`EntityWorldMut`] will observe the observer itself, which you very @@ -705,10 +691,10 @@ impl World { /// struct A; /// /// # let mut world = World::new(); - /// world.add_observer(|_: Trigger| { + /// world.add_observer(|_: On| { /// // ... /// }); - /// world.add_observer(|_: Trigger| { + /// world.add_observer(|_: On| { /// // ... /// }); /// ``` @@ -1002,7 +988,7 @@ mod tests { use crate::component::ComponentId; use crate::{ change_detection::MaybeLocation, - observer::{Observer, OnReplace}, + observer::{Observer, Replace}, prelude::*, traversal::Traversal, }; @@ -1056,14 +1042,12 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); let entity = world.spawn(A).id(); world.despawn(entity); @@ -1078,14 +1062,12 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); let mut entity = world.spawn_empty(); entity.insert(A); @@ -1102,14 +1084,12 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); let mut entity = world.spawn_empty(); entity.insert(S); @@ -1128,14 +1108,12 @@ mod tests { let entity = world.spawn(A).id(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut // and therefore does not automatically flush. @@ -1152,25 +1130,25 @@ mod tests { let mut world = World::new(); world.init_resource::(); world.add_observer( - |obs: Trigger, mut res: ResMut, mut commands: Commands| { + |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("add_a"); commands.entity(obs.target().unwrap()).insert(B); }, ); world.add_observer( - |obs: Trigger, mut res: ResMut, mut commands: Commands| { + |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("remove_a"); commands.entity(obs.target().unwrap()).remove::(); }, ); world.add_observer( - |obs: Trigger, mut res: ResMut, mut commands: Commands| { + |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("add_b"); commands.entity(obs.target().unwrap()).remove::(); }, ); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("remove_b"); }); @@ -1188,9 +1166,9 @@ mod tests { fn observer_trigger_ref() { let mut world = World::new(); - world.add_observer(|mut trigger: Trigger| trigger.event_mut().counter += 1); - world.add_observer(|mut trigger: Trigger| trigger.event_mut().counter += 2); - world.add_observer(|mut trigger: Trigger| trigger.event_mut().counter += 4); + world.add_observer(|mut trigger: On| trigger.event_mut().counter += 1); + world.add_observer(|mut trigger: On| trigger.event_mut().counter += 2); + world.add_observer(|mut trigger: On| trigger.event_mut().counter += 4); // This flush is required for the last observer to be called when triggering the event, // due to `World::add_observer` returning `WorldEntityMut`. world.flush(); @@ -1204,13 +1182,13 @@ mod tests { fn observer_trigger_targets_ref() { let mut world = World::new(); - world.add_observer(|mut trigger: Trigger| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 1; }); - world.add_observer(|mut trigger: Trigger| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 2; }); - world.add_observer(|mut trigger: Trigger| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 4; }); // This flush is required for the last observer to be called when triggering the event, @@ -1228,8 +1206,8 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add_1")); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add_2")); + world.add_observer(|_: On, mut res: ResMut| res.observed("add_1")); + world.add_observer(|_: On, mut res: ResMut| res.observed("add_2")); world.spawn(A).flush(); assert_eq!(vec!["add_2", "add_1"], world.resource::().0); @@ -1241,11 +1219,11 @@ mod tests { fn observer_multiple_events() { let mut world = World::new(); world.init_resource::(); - let on_remove = OnRemove::register_component_id(&mut world); + let on_remove = Remove::register_component_id(&mut world); world.spawn( - // SAFETY: OnAdd and OnRemove are both unit types, so this is safe + // SAFETY: Add and Remove are both unit types, so this is safe unsafe { - Observer::new(|_: Trigger, mut res: ResMut| { + Observer::new(|_: On, mut res: ResMut| { res.observed("add/remove"); }) .with_event(on_remove) @@ -1267,7 +1245,7 @@ mod tests { world.register_component::(); world.register_component::(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("add_ab"); }); @@ -1281,7 +1259,7 @@ mod tests { fn observer_despawn() { let mut world = World::new(); - let system: fn(Trigger) = |_| { + let system: fn(On) = |_| { panic!("Observer triggered after being despawned."); }; let observer = world.add_observer(system).id(); @@ -1297,11 +1275,11 @@ mod tests { let entity = world.spawn((A, B)).flush(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("remove_a"); }); - let system: fn(Trigger) = |_: Trigger| { + let system: fn(On) = |_: On| { panic!("Observer triggered after being despawned."); }; @@ -1318,7 +1296,7 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("add_ab"); }); @@ -1331,11 +1309,11 @@ mod tests { let mut world = World::new(); world.init_resource::(); - let system: fn(Trigger) = |_| { + let system: fn(On) = |_| { panic!("Trigger routed to non-targeted entity."); }; world.spawn_empty().observe(system); - world.add_observer(move |obs: Trigger, mut res: ResMut| { + world.add_observer(move |obs: On, mut res: ResMut| { assert_eq!(obs.target(), None); res.observed("event_a"); }); @@ -1353,16 +1331,16 @@ mod tests { let mut world = World::new(); world.init_resource::(); - let system: fn(Trigger) = |_| { + let system: fn(On) = |_| { panic!("Trigger routed to non-targeted entity."); }; world.spawn_empty().observe(system); let entity = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.observed("a_1")) + .observe(|_: On, mut res: ResMut| res.observed("a_1")) .id(); - world.add_observer(move |obs: Trigger, mut res: ResMut| { + world.add_observer(move |obs: On, mut res: ResMut| { assert_eq!(obs.target().unwrap(), entity); res.observed("a_2"); }); @@ -1388,26 +1366,26 @@ mod tests { // targets (entity_1, A) let entity_1 = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .observe(|_: On, mut res: ResMut| res.0 += 1) .id(); // targets (entity_2, B) let entity_2 = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.0 += 10) + .observe(|_: On, mut res: ResMut| res.0 += 10) .id(); // targets any entity or component - world.add_observer(|_: Trigger, mut res: ResMut| res.0 += 100); + world.add_observer(|_: On, mut res: ResMut| res.0 += 100); // targets any entity, and components A or B - world.add_observer(|_: Trigger, mut res: ResMut| res.0 += 1000); + world.add_observer(|_: On, mut res: ResMut| res.0 += 1000); // test all tuples - world.add_observer(|_: Trigger, mut res: ResMut| res.0 += 10000); + world.add_observer(|_: On, mut res: ResMut| res.0 += 10000); world.add_observer( - |_: Trigger, mut res: ResMut| { + |_: On, mut res: ResMut| { res.0 += 100000; }, ); world.add_observer( - |_: Trigger, + |_: On, mut res: ResMut| res.0 += 1000000, ); @@ -1495,7 +1473,7 @@ mod tests { let component_id = world.register_component::(); world.spawn( - Observer::new(|_: Trigger, mut res: ResMut| res.observed("event_a")) + Observer::new(|_: On, mut res: ResMut| res.observed("event_a")) .with_component(component_id), ); @@ -1515,7 +1493,7 @@ mod tests { fn observer_dynamic_trigger() { let mut world = World::new(); world.init_resource::(); - let event_a = OnRemove::register_component_id(&mut world); + let event_a = Remove::register_component_id(&mut world); // SAFETY: we registered `event_a` above and it matches the type of EventA let observe = unsafe { @@ -1541,14 +1519,14 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); let child = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child"); }) .id(); @@ -1568,14 +1546,14 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); let child = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child"); }) .id(); @@ -1598,14 +1576,14 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); let child = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child"); }) .id(); @@ -1628,7 +1606,7 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); @@ -1636,7 +1614,7 @@ mod tests { let child = world .spawn(ChildOf(parent)) .observe( - |mut trigger: Trigger, mut res: ResMut| { + |mut trigger: On, mut res: ResMut| { res.observed("child"); trigger.propagate(false); }, @@ -1658,21 +1636,21 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); let child_a = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child_a"); }) .id(); let child_b = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child_b"); }) .id(); @@ -1695,7 +1673,7 @@ mod tests { let entity = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("event"); }) .id(); @@ -1715,7 +1693,7 @@ mod tests { let parent_a = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent_a"); }) .id(); @@ -1723,7 +1701,7 @@ mod tests { let child_a = world .spawn(ChildOf(parent_a)) .observe( - |mut trigger: Trigger, mut res: ResMut| { + |mut trigger: On, mut res: ResMut| { res.observed("child_a"); trigger.propagate(false); }, @@ -1732,14 +1710,14 @@ mod tests { let parent_b = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent_b"); }) .id(); let child_b = world .spawn(ChildOf(parent_b)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child_b"); }) .id(); @@ -1760,7 +1738,7 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("event"); }); @@ -1782,7 +1760,7 @@ mod tests { world.init_resource::(); world.add_observer( - |trigger: Trigger, query: Query<&A>, mut res: ResMut| { + |trigger: On, query: Query<&A>, mut res: ResMut| { if query.get(trigger.target().unwrap()).is_ok() { res.observed("event"); } @@ -1804,7 +1782,7 @@ mod tests { // Originally for https://github.com/bevyengine/bevy/issues/18452 #[test] fn observer_modifies_relationship() { - fn on_add(trigger: Trigger, mut commands: Commands) { + fn on_add(trigger: On, mut commands: Commands) { commands .entity(trigger.target().unwrap()) .with_related_entities::(|rsc| { @@ -1825,7 +1803,7 @@ mod tests { let mut world = World::new(); // Observe the removal of A - this will run during despawn - world.add_observer(|_: Trigger, mut cmd: Commands| { + world.add_observer(|_: On, mut cmd: Commands| { // Spawn a new entity - this reserves a new ID and requires a flush // afterward before Entities::free can be called. cmd.spawn_empty(); @@ -1833,7 +1811,7 @@ mod tests { let ent = world.spawn(A).id(); - // Despawn our entity, which runs the OnRemove observer and allocates a + // Despawn our entity, which runs the Remove observer and allocates a // new Entity. // Should not panic - if it does, then Entities was not flushed properly // after the observer's spawn_empty. @@ -1851,7 +1829,7 @@ mod tests { let mut world = World::new(); // This fails because `ResA` is not present in the world - world.add_observer(|_: Trigger, _: Res, mut commands: Commands| { + world.add_observer(|_: On, _: Res, mut commands: Commands| { commands.insert_resource(ResB); }); world.trigger(EventA); @@ -1864,7 +1842,7 @@ mod tests { let mut world = World::new(); world.add_observer( - |_: Trigger, mut params: ParamSet<(Query, Commands)>| { + |_: On, mut params: ParamSet<(Query, Commands)>| { params.p1().insert_resource(ResA); }, ); @@ -1885,7 +1863,7 @@ mod tests { let caller = MaybeLocation::caller(); let mut world = World::new(); - world.add_observer(move |trigger: Trigger| { + world.add_observer(move |trigger: On| { assert_eq!(trigger.caller(), caller); }); world.trigger(EventA); @@ -1899,10 +1877,10 @@ mod tests { let caller = MaybeLocation::caller(); let mut world = World::new(); - world.add_observer(move |trigger: Trigger| { + world.add_observer(move |trigger: On| { assert_eq!(trigger.caller(), caller); }); - world.add_observer(move |trigger: Trigger| { + world.add_observer(move |trigger: On| { assert_eq!(trigger.caller(), caller); }); world.commands().spawn(Component).clear(); @@ -1920,7 +1898,7 @@ mod tests { let b_id = world.register_component::(); world.add_observer( - |trigger: Trigger, mut counter: ResMut| { + |trigger: On, mut counter: ResMut| { for &component in trigger.components() { *counter.0.entry(component).or_default() += 1; } diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index 61cc0973f3..283f516d51 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -28,8 +28,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// /// # Usage /// -/// The simplest usage -/// of the observer pattern looks like this: +/// The simplest usage of the observer pattern looks like this: /// /// ``` /// # use bevy_ecs::prelude::*; @@ -39,7 +38,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// message: String, /// } /// -/// world.add_observer(|trigger: Trigger| { +/// world.add_observer(|trigger: On| { /// println!("{}", trigger.event().message); /// }); /// @@ -60,8 +59,8 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # #[derive(Event)] /// # struct Speak; /// // These are functionally the same: -/// world.add_observer(|trigger: Trigger| {}); -/// world.spawn(Observer::new(|trigger: Trigger| {})); +/// world.add_observer(|trigger: On| {}); +/// world.spawn(Observer::new(|trigger: On| {})); /// ``` /// /// Observers are systems. They can access arbitrary [`World`] data by adding [`SystemParam`]s: @@ -73,14 +72,14 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # struct PrintNames; /// # #[derive(Component, Debug)] /// # struct Name; -/// world.add_observer(|trigger: Trigger, names: Query<&Name>| { +/// world.add_observer(|trigger: On, names: Query<&Name>| { /// for name in &names { /// println!("{name:?}"); /// } /// }); /// ``` /// -/// Note that [`Trigger`] must always be the first parameter. +/// Note that [`On`] must always be the first parameter. /// /// You can also add [`Commands`], which means you can spawn new entities, insert new components, etc: /// @@ -91,7 +90,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # struct SpawnThing; /// # #[derive(Component, Debug)] /// # struct Thing; -/// world.add_observer(|trigger: Trigger, mut commands: Commands| { +/// world.add_observer(|trigger: On, mut commands: Commands| { /// commands.spawn(Thing); /// }); /// ``` @@ -105,7 +104,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # struct A; /// # #[derive(Event)] /// # struct B; -/// world.add_observer(|trigger: Trigger, mut commands: Commands| { +/// world.add_observer(|trigger: On, mut commands: Commands| { /// commands.trigger(B); /// }); /// ``` @@ -123,7 +122,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// #[derive(Event)] /// struct Explode; /// -/// world.add_observer(|trigger: Trigger, mut commands: Commands| { +/// world.add_observer(|trigger: On, mut commands: Commands| { /// println!("Entity {} goes BOOM!", trigger.target().unwrap()); /// commands.entity(trigger.target().unwrap()).despawn(); /// }); @@ -156,12 +155,12 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # let e2 = world.spawn_empty().id(); /// # #[derive(Event)] /// # struct Explode; -/// world.entity_mut(e1).observe(|trigger: Trigger, mut commands: Commands| { +/// world.entity_mut(e1).observe(|trigger: On, mut commands: Commands| { /// println!("Boom!"); /// commands.entity(trigger.target().unwrap()).despawn(); /// }); /// -/// world.entity_mut(e2).observe(|trigger: Trigger, mut commands: Commands| { +/// world.entity_mut(e2).observe(|trigger: On, mut commands: Commands| { /// println!("The explosion fizzles! This entity is immune!"); /// }); /// ``` @@ -178,7 +177,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # let entity = world.spawn_empty().id(); /// # #[derive(Event)] /// # struct Explode; -/// let mut observer = Observer::new(|trigger: Trigger| {}); +/// let mut observer = Observer::new(|trigger: On| {}); /// observer.watch_entity(entity); /// world.spawn(observer); /// ``` @@ -351,7 +350,7 @@ fn observer_system_runner>( } state.last_trigger_id = last_trigger; - let trigger: Trigger = Trigger::new( + let trigger: On = On::new( // SAFETY: Caller ensures `ptr` is castable to `&mut T` unsafe { ptr.deref_mut() }, propagate, @@ -448,7 +447,7 @@ mod tests { use crate::{ error::{ignore, DefaultErrorHandler}, event::Event, - observer::Trigger, + observer::On, }; #[derive(Event)] @@ -457,7 +456,7 @@ mod tests { #[test] #[should_panic(expected = "I failed!")] fn test_fallible_observer() { - fn system(_: Trigger) -> Result { + fn system(_: On) -> Result { Err("I failed!".into()) } @@ -472,7 +471,7 @@ mod tests { #[derive(Resource, Default)] struct Ran(bool); - fn system(_: Trigger, mut ran: ResMut) -> Result { + fn system(_: On, mut ran: ResMut) -> Result { ran.0 = true; Err("I failed!".into()) } @@ -500,7 +499,7 @@ mod tests { expected = "Exclusive system `bevy_ecs::observer::runner::tests::exclusive_system_cannot_be_observer::system` may not be used as observer.\nInstead of `&mut World`, use either `DeferredWorld` if you do not need structural changes, or `Commands` if you do." )] fn exclusive_system_cannot_be_observer() { - fn system(_: Trigger, _world: &mut World) {} + fn system(_: On, _world: &mut World) {} let mut world = World::default(); world.add_observer(system); } diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs index af7b88edfc..68aa6c4310 100644 --- a/crates/bevy_ecs/src/system/commands/command.rs +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -208,7 +208,9 @@ pub fn run_schedule(label: impl ScheduleLabel) -> impl Command { } } -/// A [`Command`] that sends a global [`Trigger`](crate::observer::Trigger) without any targets. +/// A [`Command`] that sends a global [observer] [`Event`] without any targets. +/// +/// [observer]: crate::observer::Observer #[track_caller] pub fn trigger(event: impl Event) -> impl Command { let caller = MaybeLocation::caller(); @@ -217,7 +219,9 @@ pub fn trigger(event: impl Event) -> impl Command { } } -/// A [`Command`] that sends a [`Trigger`](crate::observer::Trigger) for the given targets. +/// A [`Command`] that sends an [observer] [`Event`] for the given targets. +/// +/// [observer]: crate::observer::Observer pub fn trigger_targets( event: impl Event, targets: impl TriggerTargets + Send + Sync + 'static, diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index 317ad8476a..7414b85461 100644 --- a/crates/bevy_ecs/src/system/commands/entity_command.rs +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -227,7 +227,7 @@ pub fn observe( } } -/// An [`EntityCommand`] that sends a [`Trigger`](crate::observer::Trigger) targeting an entity. +/// An [`EntityCommand`] that sends an [observer](crate::observer::Observer) [`Event`] targeting an entity. /// /// This will run any [`Observer`](crate::observer::Observer) of the given [`Event`] watching the entity. #[track_caller] diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 3012a65458..ed9ef3186c 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1068,7 +1068,7 @@ impl<'w, 's> Commands<'w, 's> { self.queue(command::run_system_cached_with(system, input).handle_error_with(warn)); } - /// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets. + /// Sends a global [observer](Observer) [`Event`] without any targets. /// /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. #[track_caller] @@ -1076,7 +1076,7 @@ impl<'w, 's> Commands<'w, 's> { self.queue(command::trigger(event)); } - /// Sends a [`Trigger`](crate::observer::Trigger) for the given targets. + /// Sends an [observer](Observer) [`Event`] for the given targets. /// /// This will run any [`Observer`] of the given [`Event`] watching those targets. #[track_caller] @@ -1091,7 +1091,7 @@ impl<'w, 's> Commands<'w, 's> { /// Spawns an [`Observer`] and returns the [`EntityCommands`] associated /// with the entity that stores the observer. /// - /// `observer` can be any system whose first parameter is a [`Trigger`]. + /// `observer` can be any system whose first parameter is [`On`]. /// /// **Calling [`observe`](EntityCommands::observe) on the returned /// [`EntityCommands`] will observe the observer itself, which you very @@ -1101,7 +1101,7 @@ impl<'w, 's> Commands<'w, 's> { /// /// Panics if the given system is an exclusive system. /// - /// [`Trigger`]: crate::observer::Trigger + /// [`On`]: crate::observer::On pub fn add_observer( &mut self, observer: impl IntoObserverSystem, @@ -1947,7 +1947,7 @@ impl<'a> EntityCommands<'a> { &mut self.commands } - /// Sends a [`Trigger`](crate::observer::Trigger) targeting the entity. + /// Sends an [observer](Observer) [`Event`] targeting the entity. /// /// This will run any [`Observer`] of the given [`Event`] watching this entity. #[track_caller] diff --git a/crates/bevy_ecs/src/system/input.rs b/crates/bevy_ecs/src/system/input.rs index 12087fdf6a..cb75016ee9 100644 --- a/crates/bevy_ecs/src/system/input.rs +++ b/crates/bevy_ecs/src/system/input.rs @@ -2,7 +2,7 @@ use core::ops::{Deref, DerefMut}; use variadics_please::all_tuples; -use crate::{bundle::Bundle, prelude::Trigger, system::System}; +use crate::{bundle::Bundle, prelude::On, system::System}; /// Trait for types that can be used as input to [`System`]s. /// @@ -11,7 +11,7 @@ use crate::{bundle::Bundle, prelude::Trigger, system::System}; /// - [`In`]: For values /// - [`InRef`]: For read-only references to values /// - [`InMut`]: For mutable references to values -/// - [`Trigger`]: For [`ObserverSystem`]s +/// - [`On`]: For [`ObserverSystem`]s /// - [`StaticSystemInput`]: For arbitrary [`SystemInput`]s in generic contexts /// - Tuples of [`SystemInput`]s up to 8 elements /// @@ -222,9 +222,9 @@ impl<'i, T: ?Sized> DerefMut for InMut<'i, T> { /// Used for [`ObserverSystem`]s. /// /// [`ObserverSystem`]: crate::system::ObserverSystem -impl SystemInput for Trigger<'_, E, B> { - type Param<'i> = Trigger<'i, E, B>; - type Inner<'i> = Trigger<'i, E, B>; +impl SystemInput for On<'_, E, B> { + type Param<'i> = On<'i, E, B>; + type Inner<'i> = On<'i, E, B>; fn wrap(this: Self::Inner<'_>) -> Self::Param<'_> { this diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index f79cb4f5d8..219b3de68b 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -410,7 +410,7 @@ mod tests { error::Result, lifecycle::RemovedComponents, name::Name, - prelude::{AnyOf, EntityRef, OnAdd, Trigger}, + prelude::{Add, AnyOf, EntityRef, On}, query::{Added, Changed, Or, SpawnDetails, Spawned, With, Without}, resource::Resource, schedule::{ @@ -1902,15 +1902,15 @@ mod tests { #[expect(clippy::unused_unit, reason = "this forces the () return type")] schedule.add_systems(|_query: Query<&Name>| -> () { todo!() }); - fn obs(_trigger: Trigger) { + fn obs(_trigger: On) { todo!() } world.add_observer(obs); - world.add_observer(|_trigger: Trigger| {}); - world.add_observer(|_trigger: Trigger| todo!()); + world.add_observer(|_trigger: On| {}); + world.add_observer(|_trigger: On| todo!()); #[expect(clippy::unused_unit, reason = "this forces the () return type")] - world.add_observer(|_trigger: Trigger| -> () { todo!() }); + world.add_observer(|_trigger: On| -> () { todo!() }); fn my_command(_world: &mut World) { todo!() diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index 4891a39d45..aa219664d6 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -5,7 +5,7 @@ use crate::{ component::{ComponentId, Tick}, error::Result, never::Never, - prelude::{Bundle, Trigger}, + prelude::{Bundle, On}, query::FilteredAccessSet, schedule::{Fallible, Infallible}, system::{input::SystemIn, System}, @@ -14,14 +14,14 @@ use crate::{ use super::{IntoSystem, SystemParamValidationError}; -/// Implemented for [`System`]s that have a [`Trigger`] as the first argument. +/// Implemented for [`System`]s that have [`On`] as the first argument. pub trait ObserverSystem: - System, Out = Out> + Send + 'static + System, Out = Out> + Send + 'static { } impl ObserverSystem for T where - T: System, Out = Out> + Send + 'static + T: System, Out = Out> + Send + 'static { } @@ -35,7 +35,7 @@ impl ObserverSystem for T where #[diagnostic::on_unimplemented( message = "`{Self}` cannot become an `ObserverSystem`", label = "the trait `IntoObserverSystem` is not implemented", - note = "for function `ObserverSystem`s, ensure the first argument is a `Trigger` and any subsequent ones are `SystemParam`" + note = "for function `ObserverSystem`s, ensure the first argument is `On` and any subsequent ones are `SystemParam`" )] pub trait IntoObserverSystem: Send + 'static { /// The type of [`System`] that this instance converts into. @@ -47,7 +47,7 @@ pub trait IntoObserverSystem: Send + 'st impl IntoObserverSystem for S where - S: IntoSystem, Out, M> + Send + 'static, + S: IntoSystem, Out, M> + Send + 'static, S::System: ObserverSystem, E: 'static, B: Bundle, @@ -61,7 +61,7 @@ where impl IntoObserverSystem for S where - S: IntoSystem, (), M> + Send + 'static, + S: IntoSystem, (), M> + Send + 'static, S::System: ObserverSystem, E: Send + Sync + 'static, B: Bundle, @@ -74,7 +74,7 @@ where } impl IntoObserverSystem for S where - S: IntoSystem, Never, M> + Send + 'static, + S: IntoSystem, Never, M> + Send + 'static, E: Send + Sync + 'static, B: Bundle, { @@ -108,7 +108,7 @@ where B: Bundle, Out: Send + Sync + 'static, { - type In = Trigger<'static, E, B>; + type In = On<'static, E, B>; type Out = Result; #[inline] @@ -189,7 +189,7 @@ where mod tests { use crate::{ event::Event, - observer::Trigger, + observer::On, system::{In, IntoSystem}, world::World, }; @@ -199,7 +199,7 @@ mod tests { #[test] fn test_piped_observer_systems_no_input() { - fn a(_: Trigger) {} + fn a(_: On) {} fn b() {} let mut world = World::new(); @@ -208,7 +208,7 @@ mod tests { #[test] fn test_piped_observer_systems_with_inputs() { - fn a(_: Trigger) -> u32 { + fn a(_: On) -> u32 { 3 } fn b(_: In) {} diff --git a/crates/bevy_ecs/src/traversal.rs b/crates/bevy_ecs/src/traversal.rs index 306ae7c92d..86772a3b18 100644 --- a/crates/bevy_ecs/src/traversal.rs +++ b/crates/bevy_ecs/src/traversal.rs @@ -14,11 +14,11 @@ use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship /// avoiding infinite loops in their code. /// /// Traversals may be parameterized with additional data. For example, in observer event propagation, the -/// parameter `D` is the event type given in `Trigger`. This allows traversal to differ depending on event +/// parameter `D` is the event type given in `On`. This allows traversal to differ depending on event /// data. /// /// [specify the direction]: crate::event::Event::Traversal -/// [event propagation]: crate::observer::Trigger::propagate +/// [event propagation]: crate::observer::On::propagate /// [observers]: crate::observer::Observer pub trait Traversal: ReadOnlyQueryData { /// Returns the next entity to visit. @@ -37,7 +37,7 @@ impl Traversal for () { /// /// Traversing in a loop could result in infinite loops for relationship graphs with loops. /// -/// [event propagation]: crate::observer::Trigger::propagate +/// [event propagation]: crate::observer::On::propagate impl Traversal for &R { fn traverse(item: Self::Item<'_>, _data: &D) -> Option { Some(item.get()) diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index da51b8e09a..acf48812c1 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -6,7 +6,7 @@ use crate::{ component::{ComponentId, Mutable}, entity::Entity, event::{Event, EventId, Events, SendBatchIds}, - lifecycle::{HookContext, ON_INSERT, ON_REPLACE}, + lifecycle::{HookContext, INSERT, REPLACE}, observer::{Observers, TriggerTargets}, prelude::{Component, QueryState}, query::{QueryData, QueryFilter}, @@ -85,7 +85,7 @@ impl<'w> DeferredWorld<'w> { /// Temporarily removes a [`Component`] `T` from the provided [`Entity`] and /// runs the provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -115,7 +115,7 @@ impl<'w> DeferredWorld<'w> { /// Temporarily removes a [`Component`] identified by the provided /// [`ComponentId`] from the provided [`Entity`] and runs the provided /// closure on it, returning the result if the component was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -145,7 +145,7 @@ impl<'w> DeferredWorld<'w> { // - DeferredWorld ensures archetype pointer will remain valid as no // relocations will occur. // - component_id exists on this world and this entity - // - ON_REPLACE is able to accept ZST events + // - REPLACE is able to accept ZST events unsafe { let archetype = &*archetype; self.trigger_on_replace( @@ -157,7 +157,7 @@ impl<'w> DeferredWorld<'w> { ); if archetype.has_replace_observer() { self.trigger_observers( - ON_REPLACE, + REPLACE, Some(entity), [component_id].into_iter(), MaybeLocation::caller(), @@ -185,7 +185,7 @@ impl<'w> DeferredWorld<'w> { // - DeferredWorld ensures archetype pointer will remain valid as no // relocations will occur. // - component_id exists on this world and this entity - // - ON_REPLACE is able to accept ZST events + // - REPLACE is able to accept ZST events unsafe { let archetype = &*archetype; self.trigger_on_insert( @@ -197,7 +197,7 @@ impl<'w> DeferredWorld<'w> { ); if archetype.has_insert_observer() { self.trigger_observers( - ON_INSERT, + INSERT, Some(entity), [component_id].into_iter(), MaybeLocation::caller(), @@ -807,12 +807,12 @@ impl<'w> DeferredWorld<'w> { } } - /// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets. + /// Sends a "global" [observer](crate::observer::Observer) [`Event`] without any targets. pub fn trigger(&mut self, trigger: impl Event) { self.commands().trigger(trigger); } - /// Sends a [`Trigger`](crate::observer::Trigger) with the given `targets`. + /// Sends an [observer](crate::observer::Observer) [`Event`] with the given `targets`. pub fn trigger_targets( &mut self, trigger: impl Event, diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index af95bbf48d..44d4604331 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -14,7 +14,7 @@ use crate::{ EntityIdLocation, EntityLocation, }, event::Event, - lifecycle::{ON_DESPAWN, ON_REMOVE, ON_REPLACE}, + lifecycle::{DESPAWN, REMOVE, REPLACE}, observer::Observer, query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData}, relationship::RelationshipHookMode, @@ -1377,7 +1377,7 @@ impl<'w> EntityWorldMut<'w> { /// Temporarily removes a [`Component`] `T` from this [`Entity`] and runs the /// provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -1430,7 +1430,7 @@ impl<'w> EntityWorldMut<'w> { /// Temporarily removes a [`Component`] `T` from this [`Entity`] and runs the /// provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -2368,7 +2368,7 @@ impl<'w> EntityWorldMut<'w> { unsafe { if archetype.has_despawn_observer() { deferred_world.trigger_observers( - ON_DESPAWN, + DESPAWN, Some(self.entity), archetype.components(), caller, @@ -2382,7 +2382,7 @@ impl<'w> EntityWorldMut<'w> { ); if archetype.has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, + REPLACE, Some(self.entity), archetype.components(), caller, @@ -2397,7 +2397,7 @@ impl<'w> EntityWorldMut<'w> { ); if archetype.has_remove_observer() { deferred_world.trigger_observers( - ON_REMOVE, + REMOVE, Some(self.entity), archetype.components(), caller, @@ -5747,7 +5747,7 @@ mod tests { let mut world = World::new(); let entity = world .spawn_empty() - .observe(|trigger: Trigger, mut commands: Commands| { + .observe(|trigger: On, mut commands: Commands| { commands .entity(trigger.target().unwrap()) .insert(TestComponent(0)); @@ -5759,7 +5759,7 @@ mod tests { let mut a = world.entity_mut(entity); a.trigger(TestEvent); // this adds command to change entity archetype - a.observe(|_: Trigger| {}); // this flushes commands implicitly by spawning + a.observe(|_: On| {}); // this flushes commands implicitly by spawning let location = a.location(); assert_eq!(world.entities().get(entity), Some(location)); } @@ -5768,11 +5768,9 @@ mod tests { #[should_panic] fn location_on_despawned_entity_panics() { let mut world = World::new(); - world.add_observer( - |trigger: Trigger, mut commands: Commands| { - commands.entity(trigger.target().unwrap()).despawn(); - }, - ); + world.add_observer(|trigger: On, mut commands: Commands| { + commands.entity(trigger.target().unwrap()).despawn(); + }); let entity = world.spawn_empty().id(); let mut a = world.entity_mut(entity); a.insert(TestComponent(0)); @@ -5790,14 +5788,12 @@ mod tests { fn archetype_modifications_trigger_flush() { let mut world = World::new(); world.insert_resource(TestFlush(0)); - world.add_observer(|_: Trigger, mut commands: Commands| { + world.add_observer(|_: On, mut commands: Commands| { + commands.queue(count_flush); + }); + world.add_observer(|_: On, mut commands: Commands| { commands.queue(count_flush); }); - world.add_observer( - |_: Trigger, mut commands: Commands| { - commands.queue(count_flush); - }, - ); world.commands().queue(count_flush); let entity = world.spawn_empty().id(); assert_eq!(world.resource::().0, 1); @@ -5862,19 +5858,19 @@ mod tests { .push("OrdA hook on_remove"); } - fn ord_a_observer_on_add(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_add(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_add"); } - fn ord_a_observer_on_insert(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_insert(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_insert"); } - fn ord_a_observer_on_replace(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_replace(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_replace"); } - fn ord_a_observer_on_remove(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_remove(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_remove"); } @@ -5913,19 +5909,19 @@ mod tests { .push("OrdB hook on_remove"); } - fn ord_b_observer_on_add(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_add(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_add"); } - fn ord_b_observer_on_insert(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_insert(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_insert"); } - fn ord_b_observer_on_replace(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_replace(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_replace"); } - fn ord_b_observer_on_remove(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_remove(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_remove"); } @@ -6201,10 +6197,10 @@ mod tests { world.insert_resource(Tracker { a: false, b: false }); let entity = world.spawn(A).id(); - world.add_observer(|_: Trigger, mut tracker: ResMut| { + world.add_observer(|_: On, mut tracker: ResMut| { tracker.a = true; }); - world.add_observer(|_: Trigger, mut tracker: ResMut| { + world.add_observer(|_: On, mut tracker: ResMut| { tracker.b = true; }); diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 34d4cd9c4a..1774da9728 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -19,8 +19,8 @@ pub use crate::{ }; use crate::{ error::{DefaultErrorHandler, ErrorHandler}, - lifecycle::{ComponentHooks, ON_ADD, ON_DESPAWN, ON_INSERT, ON_REMOVE, ON_REPLACE}, - prelude::{OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace}, + lifecycle::{ComponentHooks, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, + prelude::{Add, Despawn, Insert, Remove, Replace}, }; pub use bevy_ecs_macros::FromWorld; pub use deferred_world::DeferredWorld; @@ -150,20 +150,20 @@ impl World { #[inline] fn bootstrap(&mut self) { // The order that we register these events is vital to ensure that the constants are correct! - let on_add = OnAdd::register_component_id(self); - assert_eq!(ON_ADD, on_add); + let on_add = Add::register_component_id(self); + assert_eq!(ADD, on_add); - let on_insert = OnInsert::register_component_id(self); - assert_eq!(ON_INSERT, on_insert); + let on_insert = Insert::register_component_id(self); + assert_eq!(INSERT, on_insert); - let on_replace = OnReplace::register_component_id(self); - assert_eq!(ON_REPLACE, on_replace); + let on_replace = Replace::register_component_id(self); + assert_eq!(REPLACE, on_replace); - let on_remove = OnRemove::register_component_id(self); - assert_eq!(ON_REMOVE, on_remove); + let on_remove = Remove::register_component_id(self); + assert_eq!(REMOVE, on_remove); - let on_despawn = OnDespawn::register_component_id(self); - assert_eq!(ON_DESPAWN, on_despawn); + let on_despawn = Despawn::register_component_id(self); + assert_eq!(DESPAWN, on_despawn); // This sets up `Disabled` as a disabling component, via the FromWorld impl self.init_resource::(); @@ -1273,7 +1273,7 @@ impl World { /// Temporarily removes a [`Component`] `T` from the provided [`Entity`] and /// runs the provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -1319,7 +1319,7 @@ impl World { /// Temporarily removes a [`Component`] identified by the provided /// [`ComponentId`] from the provided [`Entity`] and runs the provided /// closure on it, returning the result if the component was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 436e838fb1..b653ab4152 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -369,7 +369,7 @@ mod tests { use alloc::string::String; use bevy_ecs::{ - lifecycle::HookContext, observer::Trigger, system::RunSystemOnce, world::DeferredWorld, + lifecycle::HookContext, observer::On, system::RunSystemOnce, world::DeferredWorld, }; use bevy_input::{ keyboard::{Key, KeyCode}, @@ -391,7 +391,7 @@ mod tests { struct GatherKeyboardEvents(String); fn gather_keyboard_events( - trigger: Trigger>, + trigger: On>, mut query: Query<&mut GatherKeyboardEvents>, ) { if let Ok(mut gather) = query.get_mut(trigger.target().unwrap()) { diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index 60df130ae0..39c6e4ebcf 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -30,7 +30,7 @@ use bevy_ecs::{ component::Component, entity::Entity, hierarchy::{ChildOf, Children}, - observer::Trigger, + observer::On, query::{With, Without}, system::{Commands, Query, Res, ResMut, SystemParam}, }; @@ -337,7 +337,7 @@ fn setup_tab_navigation(mut commands: Commands, window: Query>, + mut trigger: On>, nav: TabNavigation, mut focus: ResMut, mut visible: ResMut, diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 2592936ddd..7cd0287d81 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -548,7 +548,7 @@ pub struct LightViewEntities(EntityHashMap>); // TODO: using required component pub(crate) fn add_light_view_entities( - trigger: Trigger, + trigger: On, mut commands: Commands, ) { if let Ok(mut v) = commands.get_entity(trigger.target().unwrap()) { @@ -558,7 +558,7 @@ pub(crate) fn add_light_view_entities( /// Removes [`LightViewEntities`] when light is removed. See [`add_light_view_entities`]. pub(crate) fn extracted_light_removed( - trigger: Trigger, + trigger: On, mut commands: Commands, ) { if let Ok(mut v) = commands.get_entity(trigger.target().unwrap()) { @@ -567,7 +567,7 @@ pub(crate) fn extracted_light_removed( } pub(crate) fn remove_light_view_entities( - trigger: Trigger, + trigger: On, query: Query<&LightViewEntities>, mut commands: Commands, ) { diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index f9083433be..61f9da041d 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -11,7 +11,7 @@ //! # use bevy_picking::prelude::*; //! # let mut world = World::default(); //! world.spawn_empty() -//! .observe(|trigger: Trigger>| { +//! .observe(|trigger: On>| { //! println!("I am being hovered over"); //! }); //! ``` diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 6a7576f132..a54ecb61d9 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -17,7 +17,7 @@ //! # struct MyComponent; //! # let mut world = World::new(); //! world.spawn(MyComponent) -//! .observe(|mut trigger: Trigger>| { +//! .observe(|mut trigger: On>| { //! println!("I was just clicked!"); //! // Get the underlying pointer event data //! let click_event: &Pointer = trigger.event(); @@ -39,7 +39,7 @@ //! //! When events are generated, they bubble up the entity hierarchy starting from their target, until //! they reach the root or bubbling is halted with a call to -//! [`Trigger::propagate`](bevy_ecs::observer::Trigger::propagate). See [`Observer`] for details. +//! [`On::propagate`](bevy_ecs::observer::On::propagate). See [`Observer`] for details. //! //! This allows you to run callbacks when any children of an entity are interacted with, and leads //! to succinct, expressive code: @@ -52,18 +52,18 @@ //! # struct Greeting; //! fn setup(mut commands: Commands) { //! commands.spawn(Transform::default()) -//! // Spawn your entity here, e.g. a Mesh. +//! // Spawn your entity here, e.g. a `Mesh3d`. //! // When dragged, mutate the `Transform` component on the dragged target entity: -//! .observe(|trigger: Trigger>, mut transforms: Query<&mut Transform>| { +//! .observe(|trigger: On>, mut transforms: Query<&mut Transform>| { //! let mut transform = transforms.get_mut(trigger.target().unwrap()).unwrap(); //! let drag = trigger.event(); //! transform.rotate_local_y(drag.delta.x / 50.0); //! }) -//! .observe(|trigger: Trigger>, mut commands: Commands| { +//! .observe(|trigger: On>, mut commands: Commands| { //! println!("Entity {} goes BOOM!", trigger.target().unwrap()); //! commands.entity(trigger.target().unwrap()).despawn(); //! }) -//! .observe(|trigger: Trigger>, mut events: EventWriter| { +//! .observe(|trigger: On>, mut events: EventWriter| { //! events.write(Greeting); //! }); //! } @@ -286,7 +286,7 @@ pub type PickSet = PickingSystems; /// /// Note: for any of these plugins to work, they require a picking backend to be active, /// The picking backend is responsible to turn an input, into a [`crate::backend::PointerHits`] -/// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::Trigger`]s. +/// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::On`]s. #[derive(Default)] pub struct DefaultPickingPlugins; diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index dc7aadafbc..ace2a97bf8 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -1,11 +1,11 @@ use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::entity::EntityHash; -use bevy_ecs::lifecycle::{OnAdd, OnRemove}; +use bevy_ecs::lifecycle::{Add, Remove}; use bevy_ecs::{ component::Component, entity::{ContainsEntity, Entity, EntityEquivalent}, - observer::Trigger, + observer::On, query::With, reflect::ReflectComponent, resource::Resource, @@ -94,12 +94,12 @@ impl Plugin for SyncWorldPlugin { fn build(&self, app: &mut bevy_app::App) { app.init_resource::(); app.add_observer( - |trigger: Trigger, mut pending: ResMut| { + |trigger: On, mut pending: ResMut| { pending.push(EntityRecord::Added(trigger.target().unwrap())); }, ); app.add_observer( - |trigger: Trigger, + |trigger: On, mut pending: ResMut, query: Query<&RenderEntity>| { if let Ok(e) = query.get(trigger.target().unwrap()) { @@ -491,8 +491,8 @@ mod tests { use bevy_ecs::{ component::Component, entity::Entity, - lifecycle::{OnAdd, OnRemove}, - observer::Trigger, + lifecycle::{Add, Remove}, + observer::On, query::With, system::{Query, ResMut}, world::World, @@ -513,12 +513,12 @@ mod tests { main_world.init_resource::(); main_world.add_observer( - |trigger: Trigger, mut pending: ResMut| { + |trigger: On, mut pending: ResMut| { pending.push(EntityRecord::Added(trigger.target().unwrap())); }, ); main_world.add_observer( - |trigger: Trigger, + |trigger: On, mut pending: ResMut, query: Query<&RenderEntity>| { if let Ok(e) = query.get(trigger.target().unwrap()) { diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 56d591a365..f7699e1648 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -122,7 +122,7 @@ struct RenderScreenshotsPrepared(EntityHashMap); struct RenderScreenshotsSender(Sender<(Entity, Image)>); /// Saves the captured screenshot to disk at the provided path. -pub fn save_to_disk(path: impl AsRef) -> impl FnMut(Trigger) { +pub fn save_to_disk(path: impl AsRef) -> impl FnMut(On) { let path = path.as_ref().to_owned(); move |trigger| { let img = trigger.event().deref().clone(); diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 456cb62225..4e17af510f 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -22,9 +22,9 @@ use bevy_ecs::{ }; /// Triggered on a scene's parent entity when [`crate::SceneInstance`] becomes ready to use. /// -/// See also [`Trigger`], [`SceneSpawner::instance_is_ready`]. +/// See also [`On`], [`SceneSpawner::instance_is_ready`]. /// -/// [`Trigger`]: bevy_ecs::observer::Trigger +/// [`On`]: bevy_ecs::observer::On #[derive(Clone, Copy, Debug, Eq, PartialEq, Event, Reflect)] #[reflect(Debug, PartialEq, Clone)] pub struct SceneInstanceReady { @@ -542,7 +542,7 @@ mod tests { use bevy_ecs::{ component::Component, hierarchy::Children, - observer::Trigger, + observer::On, prelude::ReflectComponent, query::With, system::{Commands, Query, Res, ResMut, RunSystemOnce}, @@ -724,7 +724,7 @@ mod tests { fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Option) { // Add observer app.world_mut().add_observer( - move |trigger: Trigger, + move |trigger: On, scene_spawner: Res, mut trigger_count: ResMut| { assert_eq!( diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs index f91cdaee59..04d5ba041d 100644 --- a/crates/bevy_ui/src/interaction_states.rs +++ b/crates/bevy_ui/src/interaction_states.rs @@ -2,8 +2,8 @@ use bevy_a11y::AccessibilityNode; use bevy_ecs::{ component::Component, - lifecycle::{OnAdd, OnInsert, OnRemove}, - observer::Trigger, + lifecycle::{Add, Insert, Remove}, + observer::On, world::DeferredWorld, }; @@ -18,10 +18,7 @@ use bevy_ecs::{ #[derive(Component, Debug, Clone, Copy, Default)] pub struct InteractionDisabled; -pub(crate) fn on_add_disabled( - trigger: Trigger, - mut world: DeferredWorld, -) { +pub(crate) fn on_add_disabled(trigger: On, mut world: DeferredWorld) { let mut entity = world.entity_mut(trigger.target().unwrap()); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_disabled(); @@ -29,7 +26,7 @@ pub(crate) fn on_add_disabled( } pub(crate) fn on_remove_disabled( - trigger: Trigger, + trigger: On, mut world: DeferredWorld, ) { let mut entity = world.entity_mut(trigger.target().unwrap()); @@ -55,7 +52,7 @@ impl Checked { } } -pub(crate) fn on_insert_is_checked(trigger: Trigger, mut world: DeferredWorld) { +pub(crate) fn on_insert_is_checked(trigger: On, mut world: DeferredWorld) { let mut entity = world.entity_mut(trigger.target().unwrap()); let checked = entity.get::().unwrap().get(); if let Some(mut accessibility) = entity.get_mut::() { @@ -66,7 +63,7 @@ pub(crate) fn on_insert_is_checked(trigger: Trigger, mut worl } } -pub(crate) fn on_remove_is_checked(trigger: Trigger, mut world: DeferredWorld) { +pub(crate) fn on_remove_is_checked(trigger: On, mut world: DeferredWorld) { let mut entity = world.entity_mut(trigger.target().unwrap()); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_toggled(accesskit::Toggled::False); diff --git a/crates/bevy_winit/src/cursor.rs b/crates/bevy_winit/src/cursor.rs index e3e4cb9f87..580c7ebee4 100644 --- a/crates/bevy_winit/src/cursor.rs +++ b/crates/bevy_winit/src/cursor.rs @@ -22,8 +22,8 @@ use bevy_ecs::{ change_detection::DetectChanges, component::Component, entity::Entity, - lifecycle::OnRemove, - observer::Trigger, + lifecycle::Remove, + observer::On, query::With, reflect::ReflectComponent, system::{Commands, Local, Query}, @@ -192,7 +192,7 @@ fn update_cursors( } /// Resets the cursor to the default icon when `CursorIcon` is removed. -fn on_remove_cursor_icon(trigger: Trigger, mut commands: Commands) { +fn on_remove_cursor_icon(trigger: On, mut commands: Commands) { // Use `try_insert` to avoid panic if the window is being destroyed. commands .entity(trigger.target().unwrap()) diff --git a/examples/3d/edit_material_on_gltf.rs b/examples/3d/edit_material_on_gltf.rs index 029ec6bf1e..11bd35bd1f 100644 --- a/examples/3d/edit_material_on_gltf.rs +++ b/examples/3d/edit_material_on_gltf.rs @@ -8,7 +8,7 @@ use bevy::{ gltf::GltfAssetLabel, math::{Dir3, Vec3}, pbr::{DirectionalLight, MeshMaterial3d, StandardMaterial}, - prelude::{Camera3d, Children, Commands, Component, Query, Res, ResMut, Transform, Trigger}, + prelude::{Camera3d, Children, Commands, Component, On, Query, Res, ResMut, Transform}, scene::{SceneInstanceReady, SceneRoot}, DefaultPlugins, }; @@ -57,7 +57,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { } fn change_material( - trigger: Trigger, + trigger: On, mut commands: Commands, children: Query<&Children>, color_override: Query<&ColorOverride>, diff --git a/examples/3d/mixed_lighting.rs b/examples/3d/mixed_lighting.rs index 4d4dab79ce..a32efe0a86 100644 --- a/examples/3d/mixed_lighting.rs +++ b/examples/3d/mixed_lighting.rs @@ -172,7 +172,7 @@ fn spawn_scene(commands: &mut Commands, asset_server: &AssetServer) { ), )) .observe( - |_: Trigger, + |_: On, mut lighting_mode_change_event_writer: EventWriter| { // When the scene loads, send a `LightingModeChanged` event so // that we set up the lightmaps. diff --git a/examples/animation/animated_mesh.rs b/examples/animation/animated_mesh.rs index d2c4fd8443..c79b9ca79e 100644 --- a/examples/animation/animated_mesh.rs +++ b/examples/animation/animated_mesh.rs @@ -62,7 +62,7 @@ fn setup_mesh_and_animation( } fn play_animation_when_ready( - trigger: Trigger, + trigger: On, mut commands: Commands, children: Query<&Children>, animations_to_play: Query<&AnimationToPlay>, diff --git a/examples/animation/animated_mesh_events.rs b/examples/animation/animated_mesh_events.rs index c0f261752e..f5e6b4dea4 100644 --- a/examples/animation/animated_mesh_events.rs +++ b/examples/animation/animated_mesh_events.rs @@ -41,7 +41,7 @@ struct Animations { struct OnStep; fn observe_on_step( - trigger: Trigger, + trigger: On, particle: Res, mut commands: Commands, transforms: Query<&GlobalTransform>, diff --git a/examples/animation/animation_events.rs b/examples/animation/animation_events.rs index 6e292c4e6d..d31068911b 100644 --- a/examples/animation/animation_events.rs +++ b/examples/animation/animation_events.rs @@ -26,7 +26,7 @@ struct MessageEvent { } fn edit_message( - trigger: Trigger, + trigger: On, text: Single<(&mut Text2d, &mut TextColor), With>, ) { let (mut text, mut color) = text.into_inner(); diff --git a/examples/ecs/entity_disabling.rs b/examples/ecs/entity_disabling.rs index f9cebede22..66de6c74f4 100644 --- a/examples/ecs/entity_disabling.rs +++ b/examples/ecs/entity_disabling.rs @@ -36,7 +36,7 @@ fn main() { struct DisableOnClick; fn disable_entities_on_click( - trigger: Trigger>, + trigger: On>, valid_query: Query<&DisableOnClick>, mut commands: Commands, ) { diff --git a/examples/ecs/error_handling.rs b/examples/ecs/error_handling.rs index 31f9838aaa..0cbad732e4 100644 --- a/examples/ecs/error_handling.rs +++ b/examples/ecs/error_handling.rs @@ -123,7 +123,7 @@ fn setup( // Observer systems can also return a `Result`. fn fallible_observer( - trigger: Trigger>, + trigger: On>, mut world: DeferredWorld, mut step: Local, ) -> Result { diff --git a/examples/ecs/hotpatching_systems.rs b/examples/ecs/hotpatching_systems.rs index 0a6b94bf8a..c70c0a0ca9 100644 --- a/examples/ecs/hotpatching_systems.rs +++ b/examples/ecs/hotpatching_systems.rs @@ -40,7 +40,7 @@ fn update_text(mut text: Single<&mut Text>) { } fn on_click( - _click: Trigger>, + _click: On>, mut color: Single<&mut TextColor>, task_sender: Res, ) { diff --git a/examples/ecs/immutable_components.rs b/examples/ecs/immutable_components.rs index e9d3c3b383..c0ee241923 100644 --- a/examples/ecs/immutable_components.rs +++ b/examples/ecs/immutable_components.rs @@ -76,7 +76,7 @@ impl NameIndex { /// inserted in the index, and its value will not change without triggering a hook. fn on_insert_name(mut world: DeferredWorld<'_>, HookContext { entity, .. }: HookContext) { let Some(&name) = world.entity(entity).get::() else { - unreachable!("OnInsert hook guarantees `Name` is available on entity") + unreachable!("Insert hook guarantees `Name` is available on entity") }; let Some(mut index) = world.get_resource_mut::() else { return; @@ -91,7 +91,7 @@ fn on_insert_name(mut world: DeferredWorld<'_>, HookContext { entity, .. }: Hook /// inserted in the index. fn on_replace_name(mut world: DeferredWorld<'_>, HookContext { entity, .. }: HookContext) { let Some(&name) = world.entity(entity).get::() else { - unreachable!("OnReplace hook guarantees `Name` is available on entity") + unreachable!("Replace hook guarantees `Name` is available on entity") }; let Some(mut index) = world.get_resource_mut::() else { return; diff --git a/examples/ecs/observer_propagation.rs b/examples/ecs/observer_propagation.rs index 6793364a29..cea184b3aa 100644 --- a/examples/ecs/observer_propagation.rs +++ b/examples/ecs/observer_propagation.rs @@ -52,7 +52,7 @@ fn setup(mut commands: Commands) { // // - **auto_propagate:** // We can also choose whether or not this event will propagate by default when triggered. If this is -// false, it will only propagate following a call to `Trigger::propagate(true)`. +// false, it will only propagate following a call to `On::propagate(true)`. #[derive(Clone, Component, Event)] #[event(traversal = &'static ChildOf, auto_propagate)] struct Attack { @@ -77,14 +77,14 @@ fn attack_armor(entities: Query>, mut commands: Commands) { } } -fn attack_hits(trigger: Trigger, name: Query<&Name>) { +fn attack_hits(trigger: On, name: Query<&Name>) { if let Ok(name) = name.get(trigger.target().unwrap()) { info!("Attack hit {}", name); } } /// A callback placed on [`Armor`], checking if it absorbed all the [`Attack`] damage. -fn block_attack(mut trigger: Trigger, armor: Query<(&Armor, &Name)>) { +fn block_attack(mut trigger: On, armor: Query<(&Armor, &Name)>) { let (armor, name) = armor.get(trigger.target().unwrap()).unwrap(); let attack = trigger.event_mut(); let damage = attack.damage.saturating_sub(**armor); @@ -104,7 +104,7 @@ fn block_attack(mut trigger: Trigger, armor: Query<(&Armor, &Name)>) { /// A callback on the armor wearer, triggered when a piece of armor is not able to block an attack, /// or the wearer is attacked directly. fn take_damage( - trigger: Trigger, + trigger: On, mut hp: Query<(&mut HitPoints, &Name)>, mut commands: Commands, mut app_exit: EventWriter, diff --git a/examples/ecs/observers.rs b/examples/ecs/observers.rs index d7bf067d4c..e27a1ad7e2 100644 --- a/examples/ecs/observers.rs +++ b/examples/ecs/observers.rs @@ -16,7 +16,7 @@ fn main() { // Observers are systems that run when an event is "triggered". This observer runs whenever // `ExplodeMines` is triggered. .add_observer( - |trigger: Trigger, + |trigger: On, mines: Query<&Mine>, index: Res, mut commands: Commands| { @@ -112,11 +112,7 @@ fn setup(mut commands: Commands) { commands.spawn(observer); } -fn on_add_mine( - trigger: Trigger, - query: Query<&Mine>, - mut index: ResMut, -) { +fn on_add_mine(trigger: On, query: Query<&Mine>, mut index: ResMut) { let mine = query.get(trigger.target().unwrap()).unwrap(); let tile = ( (mine.pos.x / CELL_SIZE).floor() as i32, @@ -130,11 +126,7 @@ fn on_add_mine( } // Remove despawned mines from our index -fn on_remove_mine( - trigger: Trigger, - query: Query<&Mine>, - mut index: ResMut, -) { +fn on_remove_mine(trigger: On, query: Query<&Mine>, mut index: ResMut) { let mine = query.get(trigger.target().unwrap()).unwrap(); let tile = ( (mine.pos.x / CELL_SIZE).floor() as i32, @@ -145,7 +137,7 @@ fn on_remove_mine( }); } -fn explode_mine(trigger: Trigger, query: Query<&Mine>, mut commands: Commands) { +fn explode_mine(trigger: On, query: Query<&Mine>, mut commands: Commands) { // If a triggered event is targeting a specific entity you can access it with `.target()` let id = trigger.target().unwrap(); let Ok(mut entity) = commands.get_entity(id) else { diff --git a/examples/ecs/removal_detection.rs b/examples/ecs/removal_detection.rs index f60452f4ea..c247d1bf9b 100644 --- a/examples/ecs/removal_detection.rs +++ b/examples/ecs/removal_detection.rs @@ -1,6 +1,6 @@ //! This example shows how you can know when a [`Component`] has been removed, so you can react to it. //! -//! When a [`Component`] is removed from an [`Entity`], all [`Observer`] with an [`OnRemove`] trigger for +//! When a [`Component`] is removed from an [`Entity`], all [`Observer`] with an [`Remove`] trigger for //! that [`Component`] will be notified. These observers will be called immediately after the //! [`Component`] is removed. For more info on observers, see the //! [observers example](https://github.com/bevyengine/bevy/blob/main/examples/ecs/observers.rs). @@ -48,8 +48,8 @@ fn remove_component( } } -fn react_on_removal(trigger: Trigger, mut query: Query<&mut Sprite>) { - // The `OnRemove` trigger was automatically called on the `Entity` that had its `MyComponent` removed. +fn react_on_removal(trigger: On, mut query: Query<&mut Sprite>) { + // The `Remove` trigger was automatically called on the `Entity` that had its `MyComponent` removed. let entity = trigger.target().unwrap(); if let Ok(mut sprite) = query.get_mut(entity) { sprite.color = Color::srgb(0.5, 1., 1.); diff --git a/examples/no_std/library/src/lib.rs b/examples/no_std/library/src/lib.rs index ec767b763d..e336d8a98e 100644 --- a/examples/no_std/library/src/lib.rs +++ b/examples/no_std/library/src/lib.rs @@ -126,7 +126,7 @@ fn tick_timers( } } -fn unwrap(trigger: Trigger, world: &mut World) { +fn unwrap(trigger: On, world: &mut World) { if let Some(mut target) = trigger .target() .and_then(|target| world.get_entity_mut(target).ok()) diff --git a/examples/picking/debug_picking.rs b/examples/picking/debug_picking.rs index 5461d9e4c9..7f8c26a4bf 100644 --- a/examples/picking/debug_picking.rs +++ b/examples/picking/debug_picking.rs @@ -47,13 +47,13 @@ fn setup_scene( )) .observe(on_click_spawn_cube) .observe( - |out: Trigger>, mut texts: Query<&mut TextColor>| { + |out: On>, mut texts: Query<&mut TextColor>| { let mut text_color = texts.get_mut(out.target().unwrap()).unwrap(); text_color.0 = Color::WHITE; }, ) .observe( - |over: Trigger>, mut texts: Query<&mut TextColor>| { + |over: On>, mut texts: Query<&mut TextColor>| { let mut color = texts.get_mut(over.target().unwrap()).unwrap(); color.0 = bevy::color::palettes::tailwind::CYAN_400.into(); }, @@ -84,7 +84,7 @@ fn setup_scene( } fn on_click_spawn_cube( - _click: Trigger>, + _click: On>, mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, @@ -101,7 +101,7 @@ fn on_click_spawn_cube( *num += 1; } -fn on_drag_rotate(drag: Trigger>, mut transforms: Query<&mut Transform>) { +fn on_drag_rotate(drag: On>, mut transforms: Query<&mut Transform>) { if let Ok(mut transform) = transforms.get_mut(drag.target().unwrap()) { transform.rotate_y(drag.delta.x * 0.02); transform.rotate_x(drag.delta.y * 0.02); diff --git a/examples/picking/mesh_picking.rs b/examples/picking/mesh_picking.rs index c9023f48b8..60d0cb64ab 100644 --- a/examples/picking/mesh_picking.rs +++ b/examples/picking/mesh_picking.rs @@ -159,7 +159,7 @@ fn setup_scene( /// Returns an observer that updates the entity's material to the one specified. fn update_material_on( new_material: Handle, -) -> impl Fn(Trigger, Query<&mut MeshMaterial3d>) { +) -> impl Fn(On, Query<&mut MeshMaterial3d>) { // An observer closure that captures `new_material`. We do this to avoid needing to write four // versions of this observer, each triggered by a different event and with a different hardcoded // material. Instead, the event type is a generic, and the material is passed in. @@ -190,7 +190,7 @@ fn rotate(mut query: Query<&mut Transform, With>, time: Res
Expand this to see alternatives ### 1. Unified `Event` Trait One option is not to have *three* separate traits (`Event`, `EntityEvent`, `BufferedEvent`), and to instead just use associated constants on `Event` to determine whether an event supports targeting and buffering or not: ```rust pub trait Event: Send + Sync + 'static { type Traversal: Traversal; const AUTO_PROPAGATE: bool = false; const TARGETED: bool = false; const BUFFERED: bool = false; fn register_component_id(world: &mut World) -> ComponentId { ... } fn component_id(world: &World) -> Option { ... } } ``` Methods can then use bounds like `where E: Event` or `where E: Event` to limit APIs to specific kinds of events. This would keep everything under one `Event` trait, but I don't think it's necessarily a good idea. It makes APIs harder to read, and docs can't easily refer to specific types of events. You can also create weird invariants: what if you specify `TARGETED = false`, but have `Traversal` and/or `AUTO_PROPAGATE` enabled? ### 2. `Event` and `Trigger` Another option is to only split the traits between buffered events and observer events, since that is the main thing people have been asking for, and they have the largest API difference. If we did this, I think we would need to make the terms *clearly* separate. We can't really use `Event` and `BufferedEvent` as the names, since it would be strange that `BufferedEvent` doesn't implement `Event`. Something like `ObserverEvent` and `BufferedEvent` could work, but it'd be more verbose. For this approach, I would instead keep `Event` for the current `EventReader`/`EventWriter` API, and call the observer event a `Trigger`, since the "trigger" terminology is already used in the observer context within Bevy (both as a noun and a verb). This is also what a long [bikeshed on Discord](https://discord.com/channels/691052431525675048/749335865876021248/1298057661878898791) seemed to land on at the end of last year. ```rust // For `EventReader`/`EventWriter` pub trait Event: Send + Sync + 'static {} // For observers pub trait Trigger: Send + Sync + 'static { type Traversal: Traversal; const AUTO_PROPAGATE: bool = false; const TARGETED: bool = false; fn register_component_id(world: &mut World) -> ComponentId { ... } fn component_id(world: &World) -> Option { ... } } ``` The problem is that "event" is just a really good term for something that "happens". Observers are rapidly becoming the more prominent API, so it'd be weird to give them the `Trigger` name and leave the good `Event` name for the less common API. So, even though a split like this seems neat on the surface, I think it ultimately wouldn't really work. We want to keep the `Event` name for observer events, and there is no good alternative for the buffered variant. (`Message` was suggested, but saying stuff like "sends a collision message" is weird.) ### 3. `GlobalEvent` + `TargetedEvent` What if instead of focusing on the buffered vs. observed split, we *only* make a distinction between global and targeted events? ```rust // A shared event trait to allow global observers to work pub trait Event: Send + Sync + 'static { fn register_component_id(world: &mut World) -> ComponentId { ... } fn component_id(world: &World) -> Option { ... } } // For buffered events and non-targeted observer events pub trait GlobalEvent: Event {} // For targeted observer events pub trait TargetedEvent: Event { type Traversal: Traversal; const AUTO_PROPAGATE: bool = false; } ``` This is actually the first approach I implemented, and it has the neat characteristic that you can only use non-targeted APIs like `trigger` with a `GlobalEvent` and targeted APIs like `trigger_targets` with a `TargetedEvent`. You have full control over whether the entity should or should not have a target, as they are fully distinct at the type-level. However, there's a few problems: - There is no type-level indication of whether a `GlobalEvent` supports buffered events or just non-targeted observer events - An `Event` on its own does literally nothing, it's just a shared trait required to make global observers accept both non-targeted and targeted events - If an event is both a `GlobalEvent` and `TargetedEvent`, global observers again have ambiguity on whether an event has a target or not, undermining some of the benefits - The names are not ideal ### 4. `Event` and `EntityEvent` We can fix some of the problems of Alternative 3 by accepting that targeted events can also be used in non-targeted contexts, and simply having the `Event` and `EntityEvent` traits: ```rust // For buffered events and non-targeted observer events pub trait Event: Send + Sync + 'static { fn register_component_id(world: &mut World) -> ComponentId { ... } fn component_id(world: &World) -> Option { ... } } // For targeted observer events pub trait EntityEvent: Event { type Traversal: Traversal; const AUTO_PROPAGATE: bool = false; } ``` This is essentially identical to this PR, just without a dedicated `BufferedEvent`. The remaining major "problem" is that there is still zero type-level indication of whether an `Event` event *actually* supports the buffered API. This leads us to the solution proposed in this PR, using `Event`, `EntityEvent`, and `BufferedEvent`.
## Conclusion The `Event` + `EntityEvent` + `BufferedEvent` split proposed in this PR aims to solve all the common problems with Bevy's current event model while keeping the "weirdness" factor minimal. It splits in terms of both the push vs. pull *and* global vs. targeted aspects, while maintaining a shared concept for an "event". ### Why I Like This - The term "event" remains as a single concept for all the different kinds of events in Bevy. - Despite all event types being "events", they use fundamentally different APIs. Instead of assuming that you can use an event type with any pattern (when only one is typically supported), you explicitly opt in to each one with dedicated traits. - Using separate traits for each type of event helps with documentation and clearer function signatures. - I can safely make assumptions on expected usage. - If I see that an event is an `EntityEvent`, I can assume that I can use `observe` on it and get targeted events. - If I see that an event is a `BufferedEvent`, I can assume that I can use `EventReader` to read events. - If I see both `EntityEvent` and `BufferedEvent`, I can assume that both APIs are supported. In summary: This allows for a unified concept for events, while limiting the different ways to use them with opt-in traits. No more guess-work involved when using APIs. ### Problems? - Because `BufferedEvent` implements `Event` (for more consistent semantics etc.), you can still use all buffered events for non-targeted observers. I think this is fine/good. The important part is that if you see that an event implements `BufferedEvent`, you know that the `EventReader`/`EventWriter` API should be supported. Whether it *also* supports other APIs is secondary. - I currently only support `trigger_targets` for an `EntityEvent`. However, you can technically target components too, without targeting any entities. I consider that such a niche and advanced use case that it's not a huge problem to only support it for `EntityEvent`s, but we could also split `trigger_targets` into `trigger_entities` and `trigger_components` if we wanted to (or implement components as entities :P). - You can still trigger an `EntityEvent` *without* targets. I consider this correct, since `Event` implements the non-targeted behavior, and it'd be weird if implementing another trait *removed* behavior. However, it does mean that global observers for entity events can technically return `Entity::PLACEHOLDER` again (since I got rid of the `Option` added in #19440 for ergonomics). I think that's enough of an edge case that it's not a huge problem, but it is worth keeping in mind. - ~~Deriving both `EntityEvent` and `BufferedEvent` for the same type currently duplicates the `Event` implementation, so you instead need to manually implement one of them.~~ Changed to always requiring `Event` to be derived. ## Related Work There are plans to implement multi-event support for observers, especially for UI contexts. [Cart's example](https://github.com/bevyengine/bevy/issues/14649#issuecomment-2960402508) API looked like this: ```rust // Truncated for brevity trigger: Trigger<( OnAdd, OnRemove, OnAdd, OnRemove, OnInsert, )>, ``` I believe this shouldn't be in conflict with this PR. If anything, this PR might *help* achieve the multi-event pattern for entity observers with fewer footguns: by statically enforcing that all of these events are `EntityEvent`s in the context of `EntityCommands::observe`, we can avoid misuse or weird cases where *some* events inside the trigger are targeted while others are not. --- benches/benches/bevy_ecs/events/iter.rs | 2 +- benches/benches/bevy_ecs/events/send.rs | 2 +- .../benches/bevy_ecs/observers/propagation.rs | 8 +- benches/benches/bevy_ecs/observers/simple.rs | 4 +- crates/bevy_a11y/src/lib.rs | 5 +- crates/bevy_animation/src/lib.rs | 20 +- crates/bevy_app/src/app.rs | 16 +- crates/bevy_app/src/sub_app.rs | 2 +- crates/bevy_asset/src/event.rs | 12 +- crates/bevy_core_widgets/src/core_button.rs | 14 +- crates/bevy_core_widgets/src/core_slider.rs | 44 +-- .../bevy_dev_tools/src/ci_testing/config.rs | 2 +- crates/bevy_dev_tools/src/picking_debug.rs | 10 +- crates/bevy_ecs/README.md | 46 +-- crates/bevy_ecs/examples/events.rs | 6 +- crates/bevy_ecs/macros/src/component.rs | 38 ++- crates/bevy_ecs/macros/src/lib.rs | 14 +- crates/bevy_ecs/src/change_detection.rs | 2 +- crates/bevy_ecs/src/event/base.rs | 314 +++++++++++++++--- crates/bevy_ecs/src/event/collections.rs | 34 +- crates/bevy_ecs/src/event/event_cursor.rs | 15 +- crates/bevy_ecs/src/event/iterators.rs | 26 +- crates/bevy_ecs/src/event/mod.rs | 15 +- crates/bevy_ecs/src/event/mut_iterators.rs | 26 +- crates/bevy_ecs/src/event/mutator.rs | 14 +- crates/bevy_ecs/src/event/reader.rs | 14 +- crates/bevy_ecs/src/event/registry.rs | 6 +- crates/bevy_ecs/src/event/writer.rs | 18 +- crates/bevy_ecs/src/lib.rs | 8 +- crates/bevy_ecs/src/lifecycle.rs | 17 +- .../bevy_ecs/src/observer/entity_observer.rs | 8 +- crates/bevy_ecs/src/observer/mod.rs | 110 +++--- crates/bevy_ecs/src/observer/runner.rs | 25 +- crates/bevy_ecs/src/schedule/condition.rs | 11 +- crates/bevy_ecs/src/schedule/mod.rs | 3 +- .../bevy_ecs/src/system/commands/command.rs | 16 +- .../src/system/commands/entity_command.rs | 10 +- crates/bevy_ecs/src/system/commands/mod.rs | 22 +- crates/bevy_ecs/src/system/function_system.rs | 4 +- crates/bevy_ecs/src/system/system_param.rs | 12 +- crates/bevy_ecs/src/traversal.rs | 2 +- crates/bevy_ecs/src/world/deferred_world.rs | 28 +- crates/bevy_ecs/src/world/entity_ref.rs | 16 +- crates/bevy_ecs/src/world/mod.rs | 13 +- crates/bevy_input/src/gamepad.rs | 22 +- crates/bevy_input/src/gestures.rs | 10 +- crates/bevy_input/src/keyboard.rs | 6 +- crates/bevy_input/src/mouse.rs | 8 +- crates/bevy_input/src/touch.rs | 4 +- crates/bevy_input_focus/src/lib.rs | 17 +- crates/bevy_pbr/src/render/light.rs | 6 +- crates/bevy_picking/src/backend.rs | 2 +- crates/bevy_picking/src/events.rs | 12 +- crates/bevy_picking/src/lib.rs | 8 +- crates/bevy_picking/src/pointer.rs | 2 +- crates/bevy_render/src/gpu_readback.rs | 6 +- crates/bevy_render/src/sync_world.rs | 8 +- .../bevy_render/src/view/window/screenshot.rs | 2 +- crates/bevy_scene/src/scene_spawner.rs | 6 +- crates/bevy_state/src/state/transitions.rs | 6 +- crates/bevy_state/src/state_scoped_events.rs | 29 +- crates/bevy_time/src/lib.rs | 9 +- crates/bevy_time/src/time.rs | 2 +- crates/bevy_ui/src/interaction_states.rs | 8 +- crates/bevy_window/src/event.rs | 45 +-- crates/bevy_winit/src/cursor.rs | 2 +- crates/bevy_winit/src/lib.rs | 10 +- crates/bevy_winit/src/state.rs | 10 +- examples/3d/edit_material_on_gltf.rs | 4 +- examples/3d/mixed_lighting.rs | 2 +- examples/3d/solari.rs | 2 +- examples/animation/animated_mesh.rs | 4 +- examples/animation/animated_mesh_events.rs | 7 +- examples/animation/animation_events.rs | 3 +- examples/app/log_layers_ecs.rs | 2 +- .../external_source_external_thread.rs | 2 +- examples/audio/pitch.rs | 2 +- examples/ecs/component_hooks.rs | 2 +- examples/ecs/entity_disabling.rs | 2 +- examples/ecs/event.rs | 6 +- examples/ecs/observer_propagation.rs | 12 +- examples/ecs/observers.rs | 18 +- examples/ecs/removal_detection.rs | 2 +- examples/ecs/send_and_receive_events.rs | 8 +- examples/games/breakout.rs | 2 +- examples/helpers/widgets.rs | 2 +- examples/no_std/library/src/lib.rs | 7 +- examples/picking/debug_picking.rs | 6 +- examples/picking/mesh_picking.rs | 6 +- examples/picking/simple_picking.rs | 6 +- examples/picking/sprite_picking.rs | 6 +- examples/testbed/3d.rs | 2 +- examples/ui/core_widgets_observers.rs | 20 +- examples/ui/directional_navigation.rs | 2 +- examples/ui/scroll.rs | 4 +- examples/ui/size_constraints.rs | 2 +- examples/ui/tab_navigation.rs | 2 +- examples/ui/viewport_node.rs | 4 +- examples/window/custom_user_event.rs | 2 +- .../migration-guides/event_split.md | 14 + release-content/release-notes/event_split.md | 121 +++++++ tests/how_to_test_systems.rs | 2 +- 102 files changed, 1003 insertions(+), 559 deletions(-) create mode 100644 release-content/migration-guides/event_split.md create mode 100644 release-content/release-notes/event_split.md 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 e2be1afed4..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(); diff --git a/benches/benches/bevy_ecs/observers/simple.rs b/benches/benches/bevy_ecs/observers/simple.rs index b3006ec924..9c26b074e5 100644 --- a/benches/benches/bevy_ecs/observers/simple.rs +++ b/benches/benches/bevy_ecs/observers/simple.rs @@ -1,7 +1,7 @@ use core::hint::black_box; use bevy_ecs::{ - event::Event, + 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) { 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/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_app/src/app.rs b/crates/bevy_app/src/app.rs index 8f1ef3c1b4..af5183b159 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -344,7 +344,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. @@ -355,7 +355,7 @@ impl App { /// # use bevy_app::prelude::*; /// # use bevy_ecs::prelude::*; /// # - /// # #[derive(Event)] + /// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// # let mut app = App::new(); /// # @@ -363,7 +363,7 @@ impl App { /// ``` pub fn add_event(&mut self) -> &mut Self where - T: Event, + T: BufferedEvent, { self.main_mut().add_event::(); self @@ -1325,7 +1325,7 @@ impl App { /// # friends_allowed: bool, /// # }; /// # - /// # #[derive(Event)] + /// # #[derive(Event, EntityEvent)] /// # struct Invite; /// # /// # #[derive(Component)] @@ -1407,7 +1407,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 @@ -1417,7 +1417,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] @@ -1485,7 +1485,7 @@ mod tests { change_detection::{DetectChanges, ResMut}, component::Component, entity::Entity, - event::{Event, EventWriter, Events}, + event::{BufferedEvent, Event, EventWriter, Events}, lifecycle::RemovedComponents, query::With, resource::Resource, @@ -1851,7 +1851,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/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/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_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs index 31afbc1aca..ec30b625f9 100644 --- a/crates/bevy_core_widgets/src/core_button.rs +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -32,7 +32,7 @@ fn button_on_key_event( q_state: Query<(&CoreButton, Has)>, mut commands: Commands, ) { - if let Ok((bstate, disabled)) = q_state.get(trigger.target().unwrap()) { + if let Ok((bstate, disabled)) = q_state.get(trigger.target()) { if !disabled { let event = &trigger.event().input; if !event.repeat @@ -52,7 +52,7 @@ fn button_on_pointer_click( mut q_state: Query<(&CoreButton, Has, Has)>, mut commands: Commands, ) { - if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target().unwrap()) { + 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 { @@ -69,7 +69,7 @@ fn button_on_pointer_down( focus_visible: Option>, mut commands: Commands, ) { - if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { trigger.propagate(false); if !disabled { if !pressed { @@ -78,7 +78,7 @@ fn button_on_pointer_down( // 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(); + focus.0 = (trigger.target() != Entity::PLACEHOLDER).then_some(trigger.target()); } if let Some(mut focus_visible) = focus_visible { focus_visible.0 = false; @@ -92,7 +92,7 @@ fn button_on_pointer_up( mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { trigger.propagate(false); if !disabled && pressed { commands.entity(button).remove::(); @@ -105,7 +105,7 @@ fn button_on_pointer_drag_end( mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { trigger.propagate(false); if !disabled && pressed { commands.entity(button).remove::(); @@ -118,7 +118,7 @@ fn button_on_pointer_cancel( mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { trigger.propagate(false); if !disabled && pressed { commands.entity(button).remove::(); diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index b230442528..63a606be78 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -3,7 +3,8 @@ use core::ops::RangeInclusive; use accesskit::{Orientation, Role}; use bevy_a11y::AccessibilityNode; use bevy_app::{App, Plugin}; -use bevy_ecs::event::Event; +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; @@ -211,13 +212,13 @@ pub(crate) fn slider_on_pointer_down( focus_visible: Option>, mut commands: Commands, ) { - if q_thumb.contains(trigger.target().unwrap()) { + 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().unwrap()) + .iter_ancestors(trigger.target()) .find(|entity| q_slider.contains(*entity)) { // Set focus to slider and hide focus ring @@ -229,14 +230,14 @@ pub(crate) fn slider_on_pointer_down( } } } else if let Ok((slider, value, range, step, node, node_target, transform, disabled)) = - q_slider.get(trigger.target().unwrap()) + 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(); + focus.0 = (trigger.target() != Entity::PLACEHOLDER).then_some(trigger.target()); } if let Some(mut focus_visible) = focus_visible { focus_visible.0 = false; @@ -248,7 +249,7 @@ pub(crate) fn slider_on_pointer_down( // Find thumb size by searching descendants for the first entity with CoreSliderThumb let thumb_size = q_children - .iter_descendants(trigger.target().unwrap()) + .iter_descendants(trigger.target()) .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x)) .unwrap_or(0.0); @@ -283,7 +284,7 @@ pub(crate) fn slider_on_pointer_down( commands.run_system_with(on_change, new_value); } else { commands - .entity(trigger.target().unwrap()) + .entity(trigger.target()) .insert(SliderValue(new_value)); } } @@ -300,7 +301,7 @@ pub(crate) fn slider_on_drag_start( With, >, ) { - if let Ok((value, mut drag, disabled)) = q_slider.get_mut(trigger.target().unwrap()) { + if let Ok((value, mut drag, disabled)) = q_slider.get_mut(trigger.target()) { trigger.propagate(false); if !disabled { drag.dragging = true; @@ -324,8 +325,7 @@ pub(crate) fn slider_on_drag( mut commands: Commands, ui_scale: Res, ) { - if let Ok((node, slider, range, transform, drag, disabled)) = - q_slider.get_mut(trigger.target().unwrap()) + if let Ok((node, slider, range, transform, drag, disabled)) = q_slider.get_mut(trigger.target()) { trigger.propagate(false); if drag.dragging && !disabled { @@ -334,7 +334,7 @@ pub(crate) fn slider_on_drag( 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().unwrap()) + .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); @@ -349,7 +349,7 @@ pub(crate) fn slider_on_drag( commands.run_system_with(on_change, new_value); } else { commands - .entity(trigger.target().unwrap()) + .entity(trigger.target()) .insert(SliderValue(new_value)); } } @@ -360,7 +360,7 @@ 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().unwrap()) { + if let Ok((_slider, mut drag)) = q_slider.get_mut(trigger.target()) { trigger.propagate(false); if drag.dragging { drag.dragging = false; @@ -379,7 +379,7 @@ fn slider_on_key_input( )>, mut commands: Commands, ) { - if let Ok((slider, value, range, step, disabled)) = q_slider.get(trigger.target().unwrap()) { + 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 { @@ -400,14 +400,14 @@ fn slider_on_key_input( } pub(crate) fn slider_on_insert(trigger: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(trigger.target().unwrap()); + 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().unwrap()); + 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()); @@ -415,7 +415,7 @@ pub(crate) fn slider_on_insert_value(trigger: On, mut world } pub(crate) fn slider_on_insert_range(trigger: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(trigger.target().unwrap()); + 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()); @@ -424,14 +424,14 @@ pub(crate) fn slider_on_insert_range(trigger: On, mut world } pub(crate) fn slider_on_insert_step(trigger: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(trigger.target().unwrap()); + 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()); } } -/// Event which can be triggered on a slider to modify the value (using the `on_change` callback). +/// 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. /// @@ -456,7 +456,7 @@ pub(crate) fn slider_on_insert_step(trigger: On, mut world: /// commands.trigger_targets(SetSliderValue::Relative(-0.25), slider); /// } /// ``` -#[derive(Event)] +#[derive(Event, EntityEvent)] pub enum SetSliderValue { /// Set the slider value to a specific value. Absolute(f32), @@ -471,7 +471,7 @@ fn slider_on_set_value( q_slider: Query<(&CoreSlider, &SliderValue, &SliderRange, Option<&SliderStep>)>, mut commands: Commands, ) { - if let Ok((slider, value, range, step)) = q_slider.get(trigger.target().unwrap()) { + 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), @@ -484,7 +484,7 @@ fn slider_on_set_value( commands.run_system_with(on_change, new_value); } else { commands - .entity(trigger.target().unwrap()) + .entity(trigger.target()) .insert(SliderValue(new_value)); } } 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 d11818dc6a..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,7 +91,7 @@ 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::, @@ -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/README.md b/crates/bevy_ecs/README.md index 6ab8d45b7f..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,39 +307,41 @@ 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: On| { - 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: On, mut commands: Commands| { - println!("Entity {} goes BOOM!", trigger.target().unwrap()); - commands.entity(trigger.target().unwrap()).despawn(); + println!("Entity {} goes BOOM!", trigger.target()); + commands.entity(trigger.target()).despawn(); }); world.flush(); 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 6a693f2ce5..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(); diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index ee7163039d..8090cff7de 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -573,11 +573,23 @@ pub(crate) fn bevy_ecs_path() -> syn::Path { } /// Implement the `Event` trait. -#[proc_macro_derive(Event, attributes(event))] +#[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 { diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 83bee583d7..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, diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index 75e86f1b03..52839f369d 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -11,33 +11,81 @@ use core::{ marker::PhantomData, }; -/// Something that "happens" and might be read / observed by app logic. +/// Something that "happens" and can be processed by app logic. /// -/// Events can be stored in an [`Events`] resource -/// You can conveniently access events using the [`EventReader`] and [`EventWriter`] system parameter. +/// Events can be triggered on a [`World`] using a method like [`trigger`](World::trigger), +/// causing any global [`Observer`] watching that event to run. This allows for push-based +/// event handling where observers are immediately notified of events as they happen. /// -/// Events can also be "triggered" on a [`World`], which will then cause any [`Observer`] of that trigger to run. +/// Additional event handling behavior can be enabled by implementing the [`EntityEvent`] +/// and [`BufferedEvent`] traits: +/// +/// - [`EntityEvent`]s support targeting specific entities, triggering any observers watching those targets. +/// They are useful for entity-specific event handlers and can even be propagated from one entity to another. +/// - [`BufferedEvent`]s support a pull-based event handling system where events are written using an [`EventWriter`] +/// and read later using an [`EventReader`]. This is an alternative to observers that allows efficient batch processing +/// of events at fixed points in a schedule. /// /// Events must be thread-safe. /// -/// ## Derive -/// This trait can be derived. -/// Adding `auto_propagate` sets [`Self::AUTO_PROPAGATE`] to true. -/// Adding `traversal = "X"` sets [`Self::Traversal`] to be of type "X". +/// # Usage +/// +/// The [`Event`] trait can be derived: /// /// ``` -/// use bevy_ecs::prelude::*; -/// +/// # use bevy_ecs::prelude::*; +/// # /// #[derive(Event)] -/// #[event(auto_propagate)] -/// struct MyEvent; +/// struct Speak { +/// message: String, +/// } /// ``` /// +/// An [`Observer`] can then be added to listen for this event type: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event)] +/// # struct Speak { +/// # message: String, +/// # } +/// # +/// # let mut world = World::new(); +/// # +/// world.add_observer(|trigger: On| { +/// println!("{}", trigger.message); +/// }); +/// ``` +/// +/// The event can be triggered on the [`World`] using the [`trigger`](World::trigger) method: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event)] +/// # struct Speak { +/// # message: String, +/// # } +/// # +/// # let mut world = World::new(); +/// # +/// # world.add_observer(|trigger: On| { +/// # println!("{}", trigger.message); +/// # }); +/// # +/// # world.flush(); +/// # +/// world.trigger(Speak { +/// message: "Hello!".to_string(), +/// }); +/// ``` +/// +/// For events that additionally need entity targeting or buffering, consider also deriving +/// [`EntityEvent`] or [`BufferedEvent`], respectively. /// /// [`World`]: crate::world::World -/// [`ComponentId`]: crate::component::ComponentId /// [`Observer`]: crate::observer::Observer -/// [`Events`]: super::Events /// [`EventReader`]: super::EventReader /// [`EventWriter`]: super::EventWriter #[diagnostic::on_unimplemented( @@ -46,19 +94,6 @@ use core::{ note = "consider annotating `{Self}` with `#[derive(Event)]`" )] pub trait Event: Send + Sync + 'static { - /// The component that describes which [`Entity`] to propagate this event to next, when [propagation] is enabled. - /// - /// [`Entity`]: crate::entity::Entity - /// [propagation]: crate::observer::On::propagate - type Traversal: Traversal; - - /// When true, this event will always attempt to propagate when [triggered], without requiring a call - /// to [`On::propagate`]. - /// - /// [triggered]: crate::system::Commands::trigger_targets - /// [`On::propagate`]: crate::observer::On::propagate - const AUTO_PROPAGATE: bool = false; - /// Generates the [`ComponentId`] for this event type. /// /// If this type has already been registered, @@ -90,6 +125,209 @@ pub trait Event: Send + Sync + 'static { } } +/// An [`Event`] that can be targeted at specific entities. +/// +/// Entity events can be triggered on a [`World`] with specific entity targets using a method +/// like [`trigger_targets`](World::trigger_targets), causing any [`Observer`] watching the event +/// for those entities to run. +/// +/// Unlike basic [`Event`]s, entity events can optionally be propagated from one entity target to another +/// based on the [`EntityEvent::Traversal`] type associated with the event. This enables use cases +/// such as bubbling events to parent entities for UI purposes. +/// +/// Entity events must be thread-safe. +/// +/// # Usage +/// +/// The [`EntityEvent`] trait can be derived. The `event` attribute can be used to further configure +/// the propagation behavior: adding `auto_propagate` sets [`EntityEvent::AUTO_PROPAGATE`] to `true`, +/// while adding `traversal = X` sets [`EntityEvent::Traversal`] to be of type `X`. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// // When the `Damage` event is triggered on an entity, bubble the event up to ancestors. +/// #[derive(Event, EntityEvent)] +/// #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// struct Damage { +/// amount: f32, +/// } +/// ``` +/// +/// An [`Observer`] can then be added to listen for this event type for the desired entity: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, EntityEvent)] +/// # #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// # struct Damage { +/// # amount: f32, +/// # } +/// # +/// # #[derive(Component)] +/// # struct Health(f32); +/// # +/// # #[derive(Component)] +/// # struct Enemy; +/// # +/// # #[derive(Component)] +/// # struct ArmorPiece; +/// # +/// # let mut world = World::new(); +/// # +/// // Spawn an enemy entity. +/// let enemy = world.spawn((Enemy, Health(100.0))).id(); +/// +/// // Spawn some armor as a child of the enemy entity. +/// // When the armor takes damage, it will bubble the event up to the enemy, +/// // which can then handle the event with its own observer. +/// let armor_piece = world +/// .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) +/// .observe(|trigger: On, mut query: Query<&mut Health>| { +/// // Note: `On::target` only exists because this is an `EntityEvent`. +/// let mut health = query.get_mut(trigger.target()).unwrap(); +/// health.0 -= trigger.amount; +/// }) +/// .id(); +/// ``` +/// +/// The event can be triggered on the [`World`] using the [`trigger_targets`](World::trigger_targets) method, +/// providing the desired entity target(s): +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, EntityEvent)] +/// # #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// # struct Damage { +/// # amount: f32, +/// # } +/// # +/// # #[derive(Component)] +/// # struct Health(f32); +/// # +/// # #[derive(Component)] +/// # struct Enemy; +/// # +/// # #[derive(Component)] +/// # struct ArmorPiece; +/// # +/// # let mut world = World::new(); +/// # +/// # let enemy = world.spawn((Enemy, Health(100.0))).id(); +/// # let armor_piece = world +/// # .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) +/// # .observe(|trigger: On, mut query: Query<&mut Health>| { +/// # // Note: `On::target` only exists because this is an `EntityEvent`. +/// # let mut health = query.get_mut(trigger.target()).unwrap(); +/// # health.0 -= trigger.amount; +/// # }) +/// # .id(); +/// # +/// # world.flush(); +/// # +/// world.trigger_targets(Damage { amount: 10.0 }, armor_piece); +/// ``` +/// +/// [`World`]: crate::world::World +/// [`TriggerTargets`]: crate::observer::TriggerTargets +/// [`Observer`]: crate::observer::Observer +/// [`Events`]: super::Events +/// [`EventReader`]: super::EventReader +/// [`EventWriter`]: super::EventWriter +#[diagnostic::on_unimplemented( + message = "`{Self}` is not an `EntityEvent`", + label = "invalid `EntityEvent`", + note = "consider annotating `{Self}` with `#[derive(Event, EntityEvent)]`" +)] +pub trait EntityEvent: Event { + /// The component that describes which [`Entity`] to propagate this event to next, when [propagation] is enabled. + /// + /// [`Entity`]: crate::entity::Entity + /// [propagation]: crate::observer::On::propagate + type Traversal: Traversal; + + /// When true, this event will always attempt to propagate when [triggered], without requiring a call + /// to [`On::propagate`]. + /// + /// [triggered]: crate::system::Commands::trigger_targets + /// [`On::propagate`]: crate::observer::On::propagate + const AUTO_PROPAGATE: bool = false; +} + +/// A buffered [`Event`] for pull-based event handling. +/// +/// Buffered events can be written with [`EventWriter`] and read using the [`EventReader`] system parameter. +/// These events are stored in the [`Events`] resource, and require periodically polling the world for new events, +/// typically in a system that runs as part of a schedule. +/// +/// While the polling imposes a small overhead, buffered events are useful for efficiently batch processing +/// a large number of events at once. This can make them more efficient than [`Event`]s used by [`Observer`]s +/// for events that happen at a high frequency or in large quantities. +/// +/// Unlike [`Event`]s triggered for observers, buffered events are evaluated at fixed points in the schedule +/// rather than immediately when they are sent. This allows for more predictable scheduling and deferring +/// event processing to a later point in time. +/// +/// Buffered events do *not* trigger observers automatically when they are written via an [`EventWriter`]. +/// However, they can still also be triggered on a [`World`] in case you want both buffered and immediate +/// event handling for the same event. +/// +/// Buffered events must be thread-safe. +/// +/// # Usage +/// +/// The [`BufferedEvent`] trait can be derived: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// #[derive(Event, BufferedEvent)] +/// struct Message(String); +/// ``` +/// +/// The event can then be written to the event buffer using an [`EventWriter`]: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, BufferedEvent)] +/// # struct Message(String); +/// # +/// fn write_hello(mut writer: EventWriter) { +/// writer.write(Message("Hello!".to_string())); +/// } +/// ``` +/// +/// Buffered events can be efficiently read using an [`EventReader`]: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, BufferedEvent)] +/// # struct Message(String); +/// # +/// fn read_messages(mut reader: EventReader) { +/// // Process all buffered events of type `Message`. +/// for Message(message) in reader.read() { +/// println!("{message}"); +/// } +/// } +/// ``` +/// +/// [`World`]: crate::world::World +/// [`Observer`]: crate::observer::Observer +/// [`Events`]: super::Events +/// [`EventReader`]: super::EventReader +/// [`EventWriter`]: super::EventWriter +#[diagnostic::on_unimplemented( + message = "`{Self}` is not an `BufferedEvent`", + label = "invalid `BufferedEvent`", + note = "consider annotating `{Self}` with `#[derive(Event, BufferedEvent)]`" +)] +pub trait BufferedEvent: Event {} + /// An internal type that implements [`Component`] for a given [`Event`] type. /// /// This exists so we can easily get access to a unique [`ComponentId`] for each [`Event`] type, @@ -116,7 +354,7 @@ struct EventWrapperComponent(PhantomData); derive(Reflect), reflect(Clone, Debug, PartialEq, Hash) )] -pub struct EventId { +pub struct EventId { /// Uniquely identifies the event associated with this ID. // This value corresponds to the order in which each event was added to the world. pub id: usize, @@ -126,21 +364,21 @@ pub struct EventId { pub(super) _marker: PhantomData, } -impl Copy for EventId {} +impl Copy for EventId {} -impl Clone for EventId { +impl Clone for EventId { fn clone(&self) -> Self { *self } } -impl fmt::Display for EventId { +impl fmt::Display for EventId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { ::fmt(self, f) } } -impl fmt::Debug for EventId { +impl fmt::Debug for EventId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, @@ -151,27 +389,27 @@ impl fmt::Debug for EventId { } } -impl PartialEq for EventId { +impl PartialEq for EventId { fn eq(&self, other: &Self) -> bool { self.id == other.id } } -impl Eq for EventId {} +impl Eq for EventId {} -impl PartialOrd for EventId { +impl PartialOrd for EventId { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for EventId { +impl Ord for EventId { fn cmp(&self, other: &Self) -> Ordering { self.id.cmp(&other.id) } } -impl Hash for EventId { +impl Hash for EventId { fn hash(&self, state: &mut H) { Hash::hash(&self.id, state); } @@ -179,7 +417,7 @@ impl Hash for EventId { #[derive(Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub(crate) struct EventInstance { +pub(crate) struct EventInstance { pub event_id: EventId, pub event: E, } diff --git a/crates/bevy_ecs/src/event/collections.rs b/crates/bevy_ecs/src/event/collections.rs index 66447b7de4..7d1854149e 100644 --- a/crates/bevy_ecs/src/event/collections.rs +++ b/crates/bevy_ecs/src/event/collections.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; use bevy_ecs::{ change_detection::MaybeLocation, - event::{Event, EventCursor, EventId, EventInstance}, + event::{BufferedEvent, EventCursor, EventId, EventInstance}, resource::Resource, }; use core::{ @@ -38,10 +38,11 @@ use { /// dropped silently. /// /// # Example -/// ``` -/// use bevy_ecs::event::{Event, Events}; /// -/// #[derive(Event)] +/// ``` +/// use bevy_ecs::event::{BufferedEvent, Event, Events}; +/// +/// #[derive(Event, BufferedEvent)] /// struct MyEvent { /// value: usize /// } @@ -91,7 +92,7 @@ use { /// [`event_update_system`]: super::event_update_system #[derive(Debug, Resource)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Resource, Default))] -pub struct Events { +pub struct Events { /// Holds the oldest still active events. /// Note that `a.start_event_count + a.len()` should always be equal to `events_b.start_event_count`. pub(crate) events_a: EventSequence, @@ -101,7 +102,7 @@ pub struct Events { } // Derived Default impl would incorrectly require E: Default -impl Default for Events { +impl Default for Events { fn default() -> Self { Self { events_a: Default::default(), @@ -111,7 +112,7 @@ impl Default for Events { } } -impl Events { +impl Events { /// Returns the index of the oldest event stored in the event buffer. pub fn oldest_event_count(&self) -> usize { self.events_a.start_event_count @@ -286,7 +287,7 @@ impl Events { } } -impl Extend for Events { +impl Extend for Events { #[track_caller] fn extend(&mut self, iter: I) where @@ -321,13 +322,13 @@ impl Extend for Events { #[derive(Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Default))] -pub(crate) struct EventSequence { +pub(crate) struct EventSequence { pub(crate) events: Vec>, pub(crate) start_event_count: usize, } // Derived Default impl would incorrectly require E: Default -impl Default for EventSequence { +impl Default for EventSequence { fn default() -> Self { Self { events: Default::default(), @@ -336,7 +337,7 @@ impl Default for EventSequence { } } -impl Deref for EventSequence { +impl Deref for EventSequence { type Target = Vec>; fn deref(&self) -> &Self::Target { @@ -344,7 +345,7 @@ impl Deref for EventSequence { } } -impl DerefMut for EventSequence { +impl DerefMut for EventSequence { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.events } @@ -357,7 +358,7 @@ pub struct SendBatchIds { _marker: PhantomData, } -impl Iterator for SendBatchIds { +impl Iterator for SendBatchIds { type Item = EventId; fn next(&mut self) -> Option { @@ -377,7 +378,7 @@ impl Iterator for SendBatchIds { } } -impl ExactSizeIterator for SendBatchIds { +impl ExactSizeIterator for SendBatchIds { fn len(&self) -> usize { self.event_count.saturating_sub(self.last_count) } @@ -385,12 +386,11 @@ impl ExactSizeIterator for SendBatchIds { #[cfg(test)] mod tests { - use crate::event::Events; - use bevy_ecs_macros::Event; + use crate::event::{BufferedEvent, Event, Events}; #[test] fn iter_current_update_events_iterates_over_current_events() { - #[derive(Event, Clone)] + #[derive(Event, BufferedEvent, Clone)] struct TestEvent; let mut test_events = Events::::default(); diff --git a/crates/bevy_ecs/src/event/event_cursor.rs b/crates/bevy_ecs/src/event/event_cursor.rs index ff15ef4931..70e19a732c 100644 --- a/crates/bevy_ecs/src/event/event_cursor.rs +++ b/crates/bevy_ecs/src/event/event_cursor.rs @@ -1,5 +1,6 @@ use bevy_ecs::event::{ - Event, EventIterator, EventIteratorWithId, EventMutIterator, EventMutIteratorWithId, Events, + BufferedEvent, EventIterator, EventIteratorWithId, EventMutIterator, EventMutIteratorWithId, + Events, }; #[cfg(feature = "multi_threaded")] use bevy_ecs::event::{EventMutParIter, EventParIter}; @@ -19,9 +20,9 @@ use core::marker::PhantomData; /// /// ``` /// use bevy_ecs::prelude::*; -/// use bevy_ecs::event::{Event, Events, EventCursor}; +/// use bevy_ecs::event::{BufferedEvent, Events, EventCursor}; /// -/// #[derive(Event, Clone, Debug)] +/// #[derive(Event, BufferedEvent, Clone, Debug)] /// struct MyEvent; /// /// /// A system that both sends and receives events using a [`Local`] [`EventCursor`]. @@ -50,12 +51,12 @@ use core::marker::PhantomData; /// [`EventReader`]: super::EventReader /// [`EventMutator`]: super::EventMutator #[derive(Debug)] -pub struct EventCursor { +pub struct EventCursor { pub(super) last_event_count: usize, pub(super) _marker: PhantomData, } -impl Default for EventCursor { +impl Default for EventCursor { fn default() -> Self { EventCursor { last_event_count: 0, @@ -64,7 +65,7 @@ impl Default for EventCursor { } } -impl Clone for EventCursor { +impl Clone for EventCursor { fn clone(&self) -> Self { EventCursor { last_event_count: self.last_event_count, @@ -73,7 +74,7 @@ impl Clone for EventCursor { } } -impl EventCursor { +impl EventCursor { /// See [`EventReader::read`](super::EventReader::read) pub fn read<'a>(&'a mut self, events: &'a Events) -> EventIterator<'a, E> { self.read_with_id(events).without_id() diff --git a/crates/bevy_ecs/src/event/iterators.rs b/crates/bevy_ecs/src/event/iterators.rs index f9ee74b8b0..c90aed2a19 100644 --- a/crates/bevy_ecs/src/event/iterators.rs +++ b/crates/bevy_ecs/src/event/iterators.rs @@ -1,15 +1,15 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::batching::BatchingStrategy; -use bevy_ecs::event::{Event, EventCursor, EventId, EventInstance, Events}; +use bevy_ecs::event::{BufferedEvent, EventCursor, EventId, EventInstance, Events}; use core::{iter::Chain, slice::Iter}; /// An iterator that yields any unread events from an [`EventReader`](super::EventReader) or [`EventCursor`]. #[derive(Debug)] -pub struct EventIterator<'a, E: Event> { +pub struct EventIterator<'a, E: BufferedEvent> { iter: EventIteratorWithId<'a, E>, } -impl<'a, E: Event> Iterator for EventIterator<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventIterator<'a, E> { type Item = &'a E; fn next(&mut self) -> Option { self.iter.next().map(|(event, _)| event) @@ -35,7 +35,7 @@ impl<'a, E: Event> Iterator for EventIterator<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventIterator<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventIterator<'a, E> { fn len(&self) -> usize { self.iter.len() } @@ -43,13 +43,13 @@ impl<'a, E: Event> ExactSizeIterator for EventIterator<'a, E> { /// An iterator that yields any unread events (and their IDs) from an [`EventReader`](super::EventReader) or [`EventCursor`]. #[derive(Debug)] -pub struct EventIteratorWithId<'a, E: Event> { +pub struct EventIteratorWithId<'a, E: BufferedEvent> { reader: &'a mut EventCursor, chain: Chain>, Iter<'a, EventInstance>>, unread: usize, } -impl<'a, E: Event> EventIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> EventIteratorWithId<'a, E> { /// Creates a new iterator that yields any `events` that have not yet been seen by `reader`. pub fn new(reader: &'a mut EventCursor, events: &'a Events) -> Self { let a_index = reader @@ -81,7 +81,7 @@ impl<'a, E: Event> EventIteratorWithId<'a, E> { } } -impl<'a, E: Event> Iterator for EventIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventIteratorWithId<'a, E> { type Item = (&'a E, EventId); fn next(&mut self) -> Option { match self @@ -131,16 +131,16 @@ impl<'a, E: Event> Iterator for EventIteratorWithId<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventIteratorWithId<'a, E> { fn len(&self) -> usize { self.unread } } -/// A parallel iterator over `Event`s. +/// A parallel iterator over `BufferedEvent`s. #[cfg(feature = "multi_threaded")] #[derive(Debug)] -pub struct EventParIter<'a, E: Event> { +pub struct EventParIter<'a, E: BufferedEvent> { reader: &'a mut EventCursor, slices: [&'a [EventInstance]; 2], batching_strategy: BatchingStrategy, @@ -149,7 +149,7 @@ pub struct EventParIter<'a, E: Event> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> EventParIter<'a, E> { +impl<'a, E: BufferedEvent> EventParIter<'a, E> { /// Creates a new parallel iterator over `events` that have not yet been seen by `reader`. pub fn new(reader: &'a mut EventCursor, events: &'a Events) -> Self { let a_index = reader @@ -248,7 +248,7 @@ impl<'a, E: Event> EventParIter<'a, E> { } } - /// Returns the number of [`Event`]s to be iterated. + /// Returns the number of [`BufferedEvent`]s to be iterated. pub fn len(&self) -> usize { self.slices.iter().map(|s| s.len()).sum() } @@ -260,7 +260,7 @@ impl<'a, E: Event> EventParIter<'a, E> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> IntoIterator for EventParIter<'a, E> { +impl<'a, E: BufferedEvent> IntoIterator for EventParIter<'a, E> { type IntoIter = EventIteratorWithId<'a, E>; type Item = ::Item; diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 3bb422b7bb..fd624d1abf 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -11,8 +11,8 @@ mod update; mod writer; pub(crate) use base::EventInstance; -pub use base::{Event, EventId}; -pub use bevy_ecs_macros::Event; +pub use base::{BufferedEvent, EntityEvent, Event, EventId}; +pub use bevy_ecs_macros::{BufferedEvent, EntityEvent, Event}; pub use collections::{Events, SendBatchIds}; pub use event_cursor::EventCursor; #[cfg(feature = "multi_threaded")] @@ -38,17 +38,20 @@ pub use writer::EventWriter; mod tests { use alloc::{vec, vec::Vec}; use bevy_ecs::{event::*, system::assert_is_read_only_system}; - use bevy_ecs_macros::Event; + use bevy_ecs_macros::BufferedEvent; - #[derive(Event, Copy, Clone, PartialEq, Eq, Debug)] + #[derive(Event, BufferedEvent, Copy, Clone, PartialEq, Eq, Debug)] struct TestEvent { i: usize, } - #[derive(Event, Clone, PartialEq, Debug, Default)] + #[derive(Event, BufferedEvent, Clone, PartialEq, Debug, Default)] struct EmptyTestEvent; - fn get_events(events: &Events, cursor: &mut EventCursor) -> Vec { + fn get_events( + events: &Events, + cursor: &mut EventCursor, + ) -> Vec { cursor.read(events).cloned().collect::>() } diff --git a/crates/bevy_ecs/src/event/mut_iterators.rs b/crates/bevy_ecs/src/event/mut_iterators.rs index 3cb531ce78..3fa8378f23 100644 --- a/crates/bevy_ecs/src/event/mut_iterators.rs +++ b/crates/bevy_ecs/src/event/mut_iterators.rs @@ -1,17 +1,17 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::batching::BatchingStrategy; -use bevy_ecs::event::{Event, EventCursor, EventId, EventInstance, Events}; +use bevy_ecs::event::{BufferedEvent, EventCursor, EventId, EventInstance, Events}; use core::{iter::Chain, slice::IterMut}; /// An iterator that yields any unread events from an [`EventMutator`] or [`EventCursor`]. /// /// [`EventMutator`]: super::EventMutator #[derive(Debug)] -pub struct EventMutIterator<'a, E: Event> { +pub struct EventMutIterator<'a, E: BufferedEvent> { iter: EventMutIteratorWithId<'a, E>, } -impl<'a, E: Event> Iterator for EventMutIterator<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventMutIterator<'a, E> { type Item = &'a mut E; fn next(&mut self) -> Option { self.iter.next().map(|(event, _)| event) @@ -37,7 +37,7 @@ impl<'a, E: Event> Iterator for EventMutIterator<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventMutIterator<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventMutIterator<'a, E> { fn len(&self) -> usize { self.iter.len() } @@ -47,13 +47,13 @@ impl<'a, E: Event> ExactSizeIterator for EventMutIterator<'a, E> { /// /// [`EventMutator`]: super::EventMutator #[derive(Debug)] -pub struct EventMutIteratorWithId<'a, E: Event> { +pub struct EventMutIteratorWithId<'a, E: BufferedEvent> { mutator: &'a mut EventCursor, chain: Chain>, IterMut<'a, EventInstance>>, unread: usize, } -impl<'a, E: Event> EventMutIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> EventMutIteratorWithId<'a, E> { /// Creates a new iterator that yields any `events` that have not yet been seen by `mutator`. pub fn new(mutator: &'a mut EventCursor, events: &'a mut Events) -> Self { let a_index = mutator @@ -84,7 +84,7 @@ impl<'a, E: Event> EventMutIteratorWithId<'a, E> { } } -impl<'a, E: Event> Iterator for EventMutIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventMutIteratorWithId<'a, E> { type Item = (&'a mut E, EventId); fn next(&mut self) -> Option { match self @@ -134,16 +134,16 @@ impl<'a, E: Event> Iterator for EventMutIteratorWithId<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventMutIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventMutIteratorWithId<'a, E> { fn len(&self) -> usize { self.unread } } -/// A parallel iterator over `Event`s. +/// A parallel iterator over `BufferedEvent`s. #[derive(Debug)] #[cfg(feature = "multi_threaded")] -pub struct EventMutParIter<'a, E: Event> { +pub struct EventMutParIter<'a, E: BufferedEvent> { mutator: &'a mut EventCursor, slices: [&'a mut [EventInstance]; 2], batching_strategy: BatchingStrategy, @@ -152,7 +152,7 @@ pub struct EventMutParIter<'a, E: Event> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> EventMutParIter<'a, E> { +impl<'a, E: BufferedEvent> EventMutParIter<'a, E> { /// Creates a new parallel iterator over `events` that have not yet been seen by `mutator`. pub fn new(mutator: &'a mut EventCursor, events: &'a mut Events) -> Self { let a_index = mutator @@ -251,7 +251,7 @@ impl<'a, E: Event> EventMutParIter<'a, E> { } } - /// Returns the number of [`Event`]s to be iterated. + /// Returns the number of [`BufferedEvent`]s to be iterated. pub fn len(&self) -> usize { self.slices.iter().map(|s| s.len()).sum() } @@ -263,7 +263,7 @@ impl<'a, E: Event> EventMutParIter<'a, E> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> IntoIterator for EventMutParIter<'a, E> { +impl<'a, E: BufferedEvent> IntoIterator for EventMutParIter<'a, E> { type IntoIter = EventMutIteratorWithId<'a, E>; type Item = ::Item; diff --git a/crates/bevy_ecs/src/event/mutator.rs b/crates/bevy_ecs/src/event/mutator.rs index e95037af5b..a9c9459119 100644 --- a/crates/bevy_ecs/src/event/mutator.rs +++ b/crates/bevy_ecs/src/event/mutator.rs @@ -1,7 +1,7 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::event::EventMutParIter; use bevy_ecs::{ - event::{Event, EventCursor, EventMutIterator, EventMutIteratorWithId, Events}, + event::{BufferedEvent, EventCursor, EventMutIterator, EventMutIteratorWithId, Events}, system::{Local, ResMut, SystemParam}, }; @@ -15,7 +15,7 @@ use bevy_ecs::{ /// ``` /// # use bevy_ecs::prelude::*; /// -/// #[derive(Event, Debug)] +/// #[derive(Event, BufferedEvent, Debug)] /// pub struct MyEvent(pub u32); // Custom event type. /// fn my_system(mut reader: EventMutator) { /// for event in reader.read() { @@ -42,13 +42,13 @@ use bevy_ecs::{ /// [`EventReader`]: super::EventReader /// [`EventWriter`]: super::EventWriter #[derive(SystemParam, Debug)] -pub struct EventMutator<'w, 's, E: Event> { +pub struct EventMutator<'w, 's, E: BufferedEvent> { pub(super) reader: Local<'s, EventCursor>, - #[system_param(validation_message = "Event not initialized")] + #[system_param(validation_message = "BufferedEvent not initialized")] events: ResMut<'w, Events>, } -impl<'w, 's, E: Event> EventMutator<'w, 's, E> { +impl<'w, 's, E: BufferedEvent> EventMutator<'w, 's, E> { /// Iterates over the events this [`EventMutator`] has not seen yet. This updates the /// [`EventMutator`]'s event counter, which means subsequent event reads will not include events /// that happened before now. @@ -69,7 +69,7 @@ impl<'w, 's, E: Event> EventMutator<'w, 's, E> { /// # use bevy_ecs::prelude::*; /// # use std::sync::atomic::{AtomicUsize, Ordering}; /// - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct MyEvent { /// value: usize, /// } @@ -116,7 +116,7 @@ impl<'w, 's, E: Event> EventMutator<'w, 's, E> { /// ``` /// # use bevy_ecs::prelude::*; /// # - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct CollisionEvent; /// /// fn play_collision_sound(mut events: EventMutator) { diff --git a/crates/bevy_ecs/src/event/reader.rs b/crates/bevy_ecs/src/event/reader.rs index 995e2ca9e9..e15b3ea9e7 100644 --- a/crates/bevy_ecs/src/event/reader.rs +++ b/crates/bevy_ecs/src/event/reader.rs @@ -1,11 +1,11 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::event::EventParIter; use bevy_ecs::{ - event::{Event, EventCursor, EventIterator, EventIteratorWithId, Events}, + event::{BufferedEvent, EventCursor, EventIterator, EventIteratorWithId, Events}, system::{Local, Res, SystemParam}, }; -/// Reads events of type `T` in order and tracks which events have already been read. +/// Reads [`BufferedEvent`]s of type `T` in order and tracks which events have already been read. /// /// # Concurrency /// @@ -14,13 +14,13 @@ use bevy_ecs::{ /// /// [`EventWriter`]: super::EventWriter #[derive(SystemParam, Debug)] -pub struct EventReader<'w, 's, E: Event> { +pub struct EventReader<'w, 's, E: BufferedEvent> { pub(super) reader: Local<'s, EventCursor>, - #[system_param(validation_message = "Event not initialized")] + #[system_param(validation_message = "BufferedEvent not initialized")] events: Res<'w, Events>, } -impl<'w, 's, E: Event> EventReader<'w, 's, E> { +impl<'w, 's, E: BufferedEvent> EventReader<'w, 's, E> { /// Iterates over the events this [`EventReader`] has not seen yet. This updates the /// [`EventReader`]'s event counter, which means subsequent event reads will not include events /// that happened before now. @@ -41,7 +41,7 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { /// # use bevy_ecs::prelude::*; /// # use std::sync::atomic::{AtomicUsize, Ordering}; /// - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct MyEvent { /// value: usize, /// } @@ -88,7 +88,7 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { /// ``` /// # use bevy_ecs::prelude::*; /// # - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct CollisionEvent; /// /// fn play_collision_sound(mut events: EventReader) { diff --git a/crates/bevy_ecs/src/event/registry.rs b/crates/bevy_ecs/src/event/registry.rs index 0beb41cd25..7889de62da 100644 --- a/crates/bevy_ecs/src/event/registry.rs +++ b/crates/bevy_ecs/src/event/registry.rs @@ -2,7 +2,7 @@ use alloc::vec::Vec; use bevy_ecs::{ change_detection::{DetectChangesMut, MutUntyped}, component::{ComponentId, Tick}, - event::{Event, Events}, + event::{BufferedEvent, Events}, resource::Resource, world::World, }; @@ -45,7 +45,7 @@ impl EventRegistry { /// /// If no instance of the [`EventRegistry`] exists in the world, this will add one - otherwise it will use /// the existing instance. - pub fn register_event(world: &mut World) { + pub fn register_event(world: &mut World) { // By initializing the resource here, we can be sure that it is present, // and receive the correct, up-to-date `ComponentId` even if it was previously removed. let component_id = world.init_resource::>(); @@ -82,7 +82,7 @@ impl EventRegistry { } /// Removes an event from the world and its associated [`EventRegistry`]. - pub fn deregister_events(world: &mut World) { + pub fn deregister_events(world: &mut World) { let component_id = world.init_resource::>(); let mut registry = world.get_resource_or_init::(); registry diff --git a/crates/bevy_ecs/src/event/writer.rs b/crates/bevy_ecs/src/event/writer.rs index 5854ab34fb..4c38401eb4 100644 --- a/crates/bevy_ecs/src/event/writer.rs +++ b/crates/bevy_ecs/src/event/writer.rs @@ -1,9 +1,9 @@ use bevy_ecs::{ - event::{Event, EventId, Events, SendBatchIds}, + event::{BufferedEvent, EventId, Events, SendBatchIds}, system::{ResMut, SystemParam}, }; -/// Sends events of type `T`. +/// Sends [`BufferedEvent`]s of type `T`. /// /// # Usage /// @@ -11,7 +11,7 @@ use bevy_ecs::{ /// ``` /// # use bevy_ecs::prelude::*; /// -/// #[derive(Event)] +/// #[derive(Event, BufferedEvent)] /// pub struct MyEvent; // Custom event type. /// fn my_system(mut writer: EventWriter) { /// writer.write(MyEvent); @@ -21,8 +21,8 @@ use bevy_ecs::{ /// ``` /// # Observers /// -/// "Buffered" Events, such as those sent directly in [`Events`] or written using [`EventWriter`], do _not_ automatically -/// trigger any [`Observer`]s watching for that event, as each [`Event`] has different requirements regarding _if_ it will +/// "Buffered" events, such as those sent directly in [`Events`] or written using [`EventWriter`], do _not_ automatically +/// trigger any [`Observer`]s watching for that event, as each [`BufferedEvent`] has different requirements regarding _if_ it will /// be triggered, and if so, _when_ it will be triggered in the schedule. /// /// # Concurrency @@ -38,7 +38,7 @@ use bevy_ecs::{ /// /// ``` /// # use bevy_ecs::{prelude::*, event::Events}; -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # pub struct MyEvent; /// fn send_untyped(mut commands: Commands) { /// // Send an event of a specific type without having to declare that @@ -59,12 +59,12 @@ use bevy_ecs::{ /// /// [`Observer`]: crate::observer::Observer #[derive(SystemParam)] -pub struct EventWriter<'w, E: Event> { - #[system_param(validation_message = "Event not initialized")] +pub struct EventWriter<'w, E: BufferedEvent> { + #[system_param(validation_message = "BufferedEvent not initialized")] events: ResMut<'w, Events>, } -impl<'w, E: Event> EventWriter<'w, E> { +impl<'w, E: BufferedEvent> EventWriter<'w, E> { /// Writes an `event`, which can later be read by [`EventReader`](super::EventReader)s. /// This method returns the [ID](`EventId`) of the written `event`. /// diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index d8f88ed4a3..e5f0e908e5 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -60,7 +60,7 @@ pub mod world; pub use bevy_ptr as ptr; #[cfg(feature = "hotpatching")] -use event::Event; +use event::{BufferedEvent, Event}; /// The ECS prelude. /// @@ -78,7 +78,9 @@ pub mod prelude { component::Component, entity::{ContainsEntity, Entity, EntityMapper}, error::{BevyError, Result}, - event::{Event, EventMutator, EventReader, EventWriter, Events}, + event::{ + BufferedEvent, EntityEvent, Event, EventMutator, EventReader, EventWriter, Events, + }, hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children}, lifecycle::{ Add, Despawn, Insert, OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace, Remove, @@ -137,7 +139,7 @@ pub mod __macro_exports { /// /// Systems should refresh their inner pointers. #[cfg(feature = "hotpatching")] -#[derive(Event, Default)] +#[derive(Event, BufferedEvent, Default)] pub struct HotPatched; #[cfg(test)] diff --git a/crates/bevy_ecs/src/lifecycle.rs b/crates/bevy_ecs/src/lifecycle.rs index 0c07995ce9..be5765beee 100644 --- a/crates/bevy_ecs/src/lifecycle.rs +++ b/crates/bevy_ecs/src/lifecycle.rs @@ -53,7 +53,10 @@ use crate::{ change_detection::MaybeLocation, component::{Component, ComponentId, ComponentIdFor, Tick}, entity::Entity, - event::{Event, EventCursor, EventId, EventIterator, EventIteratorWithId, Events}, + event::{ + BufferedEvent, EntityEvent, Event, EventCursor, EventId, EventIterator, + EventIteratorWithId, Events, + }, query::FilteredAccessSet, relationship::RelationshipHookMode, storage::SparseSet, @@ -325,7 +328,7 @@ pub const DESPAWN: ComponentId = ComponentId::new(4); /// Trigger emitted when a component is inserted onto an entity that does not already have that /// component. Runs before `Insert`. /// See [`crate::lifecycle::ComponentHooks::on_add`] for more information. -#[derive(Event, Debug)] +#[derive(Event, EntityEvent, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnAdd")] @@ -334,7 +337,7 @@ pub struct Add; /// Trigger emitted when a component is inserted, regardless of whether or not the entity already /// had that component. Runs after `Add`, if it ran. /// See [`crate::lifecycle::ComponentHooks::on_insert`] for more information. -#[derive(Event, Debug)] +#[derive(Event, EntityEvent, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnInsert")] @@ -345,7 +348,7 @@ pub struct Insert; /// /// Runs before the value is replaced, so you can still access the original component data. /// See [`crate::lifecycle::ComponentHooks::on_replace`] for more information. -#[derive(Event, Debug)] +#[derive(Event, EntityEvent, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnReplace")] @@ -354,7 +357,7 @@ pub struct Replace; /// Trigger emitted when a component is removed from an entity, and runs before the component is /// removed, so you can still access the component data. /// See [`crate::lifecycle::ComponentHooks::on_remove`] for more information. -#[derive(Event, Debug)] +#[derive(Event, EntityEvent, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnRemove")] @@ -362,7 +365,7 @@ pub struct Remove; /// Trigger emitted for each component on an entity when it is despawned. /// See [`crate::lifecycle::ComponentHooks::on_despawn`] for more information. -#[derive(Event, Debug)] +#[derive(Event, EntityEvent, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] #[doc(alias = "OnDespawn")] @@ -390,7 +393,7 @@ pub type OnDespawn = Despawn; /// Wrapper around [`Entity`] for [`RemovedComponents`]. /// Internally, `RemovedComponents` uses these as an `Events`. -#[derive(Event, Debug, Clone, Into)] +#[derive(Event, BufferedEvent, Debug, Clone, Into)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))] pub struct RemovedComponentEntity(Entity); diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_observer.rs index 0e6d9d7781..5530b1f0fc 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_observer.rs @@ -121,14 +121,18 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo #[cfg(test)] mod tests { use crate::{ - entity::EntityCloner, event::Event, observer::On, resource::Resource, system::ResMut, + entity::EntityCloner, + event::{EntityEvent, Event}, + observer::On, + resource::Resource, + system::ResMut, world::World, }; #[derive(Resource, Default)] struct Num(usize); - #[derive(Event)] + #[derive(Event, EntityEvent)] struct E; #[test] diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 0d0ca66299..d34b686b95 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -54,7 +54,7 @@ //! ## Triggering observers //! //! Observers are most commonly triggered by [`Commands`], -//! via [`Commands::trigger`] (for untargeted events) or [`Commands::trigger_targets`] (for targeted events). +//! via [`Commands::trigger`] (for untargeted [`Event`]s) or [`Commands::trigger_targets`] (for targeted [`EntityEvent`]s). //! Like usual, equivalent methods are available on [`World`], allowing you to reduce overhead when working with exclusive world access. //! //! If your observer is configured to watch for a specific component or set of components instead, @@ -64,15 +64,14 @@ //! //! ## Observer bubbling //! -//! When events are targeted at an entity, they can optionally bubble to other targets, +//! When using an [`EntityEvent`] targeted at an entity, the event can optionally be propagated to other targets, //! typically up to parents in an entity hierarchy. //! -//! This behavior is controlled via [`Event::Traversal`] and [`Event::AUTO_PROPAGATE`], +//! This behavior is controlled via [`EntityEvent::Traversal`] and [`EntityEvent::AUTO_PROPAGATE`], //! with the details of the propagation path specified by the [`Traversal`](crate::traversal::Traversal) trait. //! -//! When auto-propagation is enabled, propagaion must be manually stopped to prevent the event from -//! continuing to other targets. -//! This can be done using the [`On::propagate`] method inside of your observer. +//! When auto-propagation is enabled, propagation must be manually stopped to prevent the event from +//! continuing to other targets. This can be done using the [`On::propagate`] method inside of your observer. //! //! ## Observer timing //! @@ -212,12 +211,6 @@ impl<'w, E, B: Bundle> On<'w, E, B> { Ptr::from(&self.event) } - /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. It may - /// be [`None`] if the trigger is not for a particular entity. - pub fn target(&self) -> Option { - self.trigger.target - } - /// Returns the components that triggered the observer, out of the /// components defined in `B`. Does not necessarily include all of them as /// `B` acts like an `OR` filter rather than an `AND` filter. @@ -251,14 +244,28 @@ impl<'w, E, B: Bundle> On<'w, E, B> { self.trigger.observer } + /// Returns the source code location that triggered this observer. + pub fn caller(&self) -> MaybeLocation { + self.trigger.caller + } +} + +impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { + /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. + /// + /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. + pub fn target(&self) -> Entity { + self.trigger.target.unwrap_or(Entity::PLACEHOLDER) + } + /// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities. /// /// The path an event will propagate along is specified by its associated [`Traversal`] component. By default, events /// use `()` which ends the path immediately and prevents propagation. /// /// To enable propagation, you must: - /// + Set [`Event::Traversal`] to the component you want to propagate along. - /// + Either call `propagate(true)` in the first observer or set [`Event::AUTO_PROPAGATE`] to `true`. + /// + Set [`EntityEvent::Traversal`] to the component you want to propagate along. + /// + Either call `propagate(true)` in the first observer or set [`EntityEvent::AUTO_PROPAGATE`] to `true`. /// /// You can prevent an event from propagating further using `propagate(false)`. /// @@ -273,11 +280,6 @@ impl<'w, E, B: Bundle> On<'w, E, B> { pub fn get_propagate(&self) -> bool { *self.propagate } - - /// Returns the source code location that triggered this observer. - pub fn caller(&self) -> MaybeLocation { - self.trigger.caller - } } impl<'w, E: Debug, B: Bundle> Debug for On<'w, E, B> { @@ -723,7 +725,7 @@ impl World { let event_id = E::register_component_id(self); // SAFETY: We just registered `event_id` with the type of `event` unsafe { - self.trigger_targets_dynamic_ref_with_caller(event_id, &mut event, (), caller); + self.trigger_dynamic_ref_with_caller(event_id, &mut event, caller); } } @@ -735,20 +737,40 @@ impl World { pub fn trigger_ref(&mut self, event: &mut E) { let event_id = E::register_component_id(self); // SAFETY: We just registered `event_id` with the type of `event` - unsafe { self.trigger_targets_dynamic_ref(event_id, event, ()) }; + unsafe { self.trigger_dynamic_ref_with_caller(event_id, event, MaybeLocation::caller()) }; } - /// Triggers the given [`Event`] for the given `targets`, which will run any [`Observer`]s watching for it. + unsafe fn trigger_dynamic_ref_with_caller( + &mut self, + event_id: ComponentId, + event_data: &mut E, + caller: MaybeLocation, + ) { + let mut world = DeferredWorld::from(self); + // SAFETY: `event_data` is accessible as the type represented by `event_id` + unsafe { + world.trigger_observers_with_data::<_, ()>( + event_id, + None, + core::iter::empty::(), + event_data, + false, + caller, + ); + }; + } + + /// Triggers the given [`EntityEvent`] for the given `targets`, which will run any [`Observer`]s watching for it. /// /// While event types commonly implement [`Copy`], /// those that don't will be consumed and will no longer be accessible. /// If you need to use the event after triggering it, use [`World::trigger_targets_ref`] instead. #[track_caller] - pub fn trigger_targets(&mut self, event: E, targets: impl TriggerTargets) { + pub fn trigger_targets(&mut self, event: E, targets: impl TriggerTargets) { self.trigger_targets_with_caller(event, targets, MaybeLocation::caller()); } - pub(crate) fn trigger_targets_with_caller( + pub(crate) fn trigger_targets_with_caller( &mut self, mut event: E, targets: impl TriggerTargets, @@ -761,19 +783,23 @@ impl World { } } - /// Triggers the given [`Event`] as a mutable reference for the given `targets`, + /// Triggers the given [`EntityEvent`] as a mutable reference for the given `targets`, /// which will run any [`Observer`]s watching for it. /// /// Compared to [`World::trigger_targets`], this method is most useful when it's necessary to check /// or use the event after it has been modified by observers. #[track_caller] - pub fn trigger_targets_ref(&mut self, event: &mut E, targets: impl TriggerTargets) { + pub fn trigger_targets_ref( + &mut self, + event: &mut E, + targets: impl TriggerTargets, + ) { let event_id = E::register_component_id(self); // SAFETY: We just registered `event_id` with the type of `event` unsafe { self.trigger_targets_dynamic_ref(event_id, event, targets) }; } - /// Triggers the given [`Event`] for the given `targets`, which will run any [`Observer`]s watching for it. + /// Triggers the given [`EntityEvent`] for the given `targets`, which will run any [`Observer`]s watching for it. /// /// While event types commonly implement [`Copy`], /// those that don't will be consumed and will no longer be accessible. @@ -783,7 +809,7 @@ impl World { /// /// Caller must ensure that `event_data` is accessible as the type represented by `event_id`. #[track_caller] - pub unsafe fn trigger_targets_dynamic( + pub unsafe fn trigger_targets_dynamic( &mut self, event_id: ComponentId, mut event_data: E, @@ -795,7 +821,7 @@ impl World { }; } - /// Triggers the given [`Event`] as a mutable reference for the given `targets`, + /// Triggers the given [`EntityEvent`] as a mutable reference for the given `targets`, /// which will run any [`Observer`]s watching for it. /// /// Compared to [`World::trigger_targets_dynamic`], this method is most useful when it's necessary to check @@ -805,7 +831,7 @@ impl World { /// /// Caller must ensure that `event_data` is accessible as the type represented by `event_id`. #[track_caller] - pub unsafe fn trigger_targets_dynamic_ref( + pub unsafe fn trigger_targets_dynamic_ref( &mut self, event_id: ComponentId, event_data: &mut E, @@ -822,7 +848,7 @@ impl World { /// # Safety /// /// See `trigger_targets_dynamic_ref` - unsafe fn trigger_targets_dynamic_ref_with_caller( + unsafe fn trigger_targets_dynamic_ref_with_caller( &mut self, event_id: ComponentId, event_data: &mut E, @@ -1006,10 +1032,10 @@ mod tests { #[component(storage = "SparseSet")] struct S; - #[derive(Event)] + #[derive(Event, EntityEvent)] struct EventA; - #[derive(Event)] + #[derive(Event, EntityEvent)] struct EventWithData { counter: usize, } @@ -1033,8 +1059,8 @@ mod tests { } } - #[derive(Component, Event)] - #[event(traversal = &'static ChildOf, auto_propagate)] + #[derive(Component, Event, EntityEvent)] + #[entity_event(traversal = &'static ChildOf, auto_propagate)] struct EventPropagating; #[test] @@ -1132,20 +1158,20 @@ mod tests { world.add_observer( |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("add_a"); - commands.entity(obs.target().unwrap()).insert(B); + commands.entity(obs.target()).insert(B); }, ); world.add_observer( |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("remove_a"); - commands.entity(obs.target().unwrap()).remove::(); + commands.entity(obs.target()).remove::(); }, ); world.add_observer( |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("add_b"); - commands.entity(obs.target().unwrap()).remove::(); + commands.entity(obs.target()).remove::(); }, ); world.add_observer(|_: On, mut res: ResMut| { @@ -1314,7 +1340,7 @@ mod tests { }; world.spawn_empty().observe(system); world.add_observer(move |obs: On, mut res: ResMut| { - assert_eq!(obs.target(), None); + assert_eq!(obs.target(), Entity::PLACEHOLDER); res.observed("event_a"); }); @@ -1341,7 +1367,7 @@ mod tests { .observe(|_: On, mut res: ResMut| res.observed("a_1")) .id(); world.add_observer(move |obs: On, mut res: ResMut| { - assert_eq!(obs.target().unwrap(), entity); + assert_eq!(obs.target(), entity); res.observed("a_2"); }); @@ -1761,7 +1787,7 @@ mod tests { world.add_observer( |trigger: On, query: Query<&A>, mut res: ResMut| { - if query.get(trigger.target().unwrap()).is_ok() { + if query.get(trigger.target()).is_ok() { res.observed("event"); } }, @@ -1784,7 +1810,7 @@ mod tests { fn observer_modifies_relationship() { fn on_add(trigger: On, mut commands: Commands) { commands - .entity(trigger.target().unwrap()) + .entity(trigger.target()) .with_related_entities::(|rsc| { rsc.spawn_empty(); }); diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index 283f516d51..4fd9f23556 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -21,10 +21,12 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer". /// -/// Observers listen for a "trigger" of a specific [`Event`]. Events are triggered by calling [`World::trigger`] or [`World::trigger_targets`]. +/// Observers listen for a "trigger" of a specific [`Event`]. An event can be triggered on the [`World`] +/// by calling [`World::trigger`], or if the event is an [`EntityEvent`], it can also be triggered for specific +/// entity targets using [`World::trigger_targets`]. /// -/// Note that "buffered" events sent using [`EventReader`] and [`EventWriter`] are _not_ automatically triggered. They must be triggered at a specific -/// point in the schedule. +/// Note that [`BufferedEvent`]s sent using [`EventReader`] and [`EventWriter`] are _not_ automatically triggered. +/// They must be triggered at a specific point in the schedule. /// /// # Usage /// @@ -113,18 +115,19 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// recursively evaluated until there are no commands left, meaning nested triggers all /// evaluate at the same time! /// -/// Events can be triggered for entities, which will be passed to the [`Observer`]: +/// If the event is an [`EntityEvent`], it can be triggered for specific entities, +/// which will be passed to the [`Observer`]: /// /// ``` /// # use bevy_ecs::prelude::*; /// # let mut world = World::default(); /// # let entity = world.spawn_empty().id(); -/// #[derive(Event)] +/// #[derive(Event, EntityEvent)] /// struct Explode; /// /// world.add_observer(|trigger: On, mut commands: Commands| { -/// println!("Entity {} goes BOOM!", trigger.target().unwrap()); -/// commands.entity(trigger.target().unwrap()).despawn(); +/// println!("Entity {} goes BOOM!", trigger.target()); +/// commands.entity(trigger.target()).despawn(); /// }); /// /// world.flush(); @@ -139,7 +142,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # let mut world = World::default(); /// # let e1 = world.spawn_empty().id(); /// # let e2 = world.spawn_empty().id(); -/// # #[derive(Event)] +/// # #[derive(Event, EntityEvent)] /// # struct Explode; /// world.trigger_targets(Explode, [e1, e2]); /// ``` @@ -153,11 +156,11 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # let mut world = World::default(); /// # let e1 = world.spawn_empty().id(); /// # let e2 = world.spawn_empty().id(); -/// # #[derive(Event)] +/// # #[derive(Event, EntityEvent)] /// # struct Explode; /// world.entity_mut(e1).observe(|trigger: On, mut commands: Commands| { /// println!("Boom!"); -/// commands.entity(trigger.target().unwrap()).despawn(); +/// commands.entity(trigger.target()).despawn(); /// }); /// /// world.entity_mut(e2).observe(|trigger: On, mut commands: Commands| { @@ -175,7 +178,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # use bevy_ecs::prelude::*; /// # let mut world = World::default(); /// # let entity = world.spawn_empty().id(); -/// # #[derive(Event)] +/// # #[derive(Event, EntityEvent)] /// # struct Explode; /// let mut observer = Observer::new(|trigger: On| {}); /// observer.watch_entity(entity); diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 45eb4febb2..4983804dab 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -466,7 +466,7 @@ pub mod common_conditions { use super::{NotSystem, SystemCondition}; use crate::{ change_detection::DetectChanges, - event::{Event, EventReader}, + event::{BufferedEvent, EventReader}, lifecycle::RemovedComponents, prelude::{Component, Query, With}, query::QueryFilter, @@ -928,7 +928,7 @@ pub mod common_conditions { /// my_system.run_if(on_event::), /// ); /// - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct MyEvent; /// /// fn my_system(mut counter: ResMut) { @@ -945,7 +945,7 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 1); /// ``` - pub fn on_event(mut reader: EventReader) -> bool { + pub fn on_event(mut reader: EventReader) -> bool { // The events need to be consumed, so that there are no false positives on subsequent // calls of the run condition. Simply checking `is_empty` would not be enough. // PERF: note that `count` is efficient (not actually looping/iterating), @@ -1328,6 +1328,7 @@ where #[cfg(test)] mod tests { use super::{common_conditions::*, SystemCondition}; + use crate::event::{BufferedEvent, Event}; use crate::query::With; use crate::{ change_detection::ResMut, @@ -1336,7 +1337,7 @@ mod tests { system::Local, world::World, }; - use bevy_ecs_macros::{Event, Resource}; + use bevy_ecs_macros::Resource; #[derive(Resource, Default)] struct Counter(usize); @@ -1447,7 +1448,7 @@ mod tests { #[derive(Component)] struct TestComponent; - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct TestEvent; #[derive(Resource)] diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 8c5aa1d6fb..91f1b41312 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -784,8 +784,7 @@ mod tests { #[derive(Component)] struct B; - // An event type - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct E; #[derive(Resource, Component)] diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs index 68aa6c4310..84a2fdf4e9 100644 --- a/crates/bevy_ecs/src/system/commands/command.rs +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -9,7 +9,7 @@ use crate::{ change_detection::MaybeLocation, entity::Entity, error::Result, - event::{Event, Events}, + event::{BufferedEvent, EntityEvent, Event, Events}, observer::TriggerTargets, resource::Resource, schedule::ScheduleLabel, @@ -208,9 +208,7 @@ pub fn run_schedule(label: impl ScheduleLabel) -> impl Command { } } -/// A [`Command`] that sends a global [observer] [`Event`] without any targets. -/// -/// [observer]: crate::observer::Observer +/// A [`Command`] that sends a global [`Event`] without any targets. #[track_caller] pub fn trigger(event: impl Event) -> impl Command { let caller = MaybeLocation::caller(); @@ -219,11 +217,9 @@ pub fn trigger(event: impl Event) -> impl Command { } } -/// A [`Command`] that sends an [observer] [`Event`] for the given targets. -/// -/// [observer]: crate::observer::Observer +/// A [`Command`] that sends an [`EntityEvent`] for the given targets. pub fn trigger_targets( - event: impl Event, + event: impl EntityEvent, targets: impl TriggerTargets + Send + Sync + 'static, ) -> impl Command { let caller = MaybeLocation::caller(); @@ -232,9 +228,9 @@ pub fn trigger_targets( } } -/// A [`Command`] that sends an arbitrary [`Event`]. +/// A [`Command`] that sends an arbitrary [`BufferedEvent`]. #[track_caller] -pub fn send_event(event: E) -> impl Command { +pub fn send_event(event: E) -> impl Command { let caller = MaybeLocation::caller(); move |world: &mut World| { let mut events = world.resource_mut::>(); diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index 7414b85461..87bd2d858b 100644 --- a/crates/bevy_ecs/src/system/commands/entity_command.rs +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -12,7 +12,7 @@ use crate::{ change_detection::MaybeLocation, component::{Component, ComponentId, ComponentInfo}, entity::{Entity, EntityClonerBuilder}, - event::Event, + event::EntityEvent, relationship::RelationshipHookMode, system::IntoObserverSystem, world::{error::EntityMutableFetchError, EntityWorldMut, FromWorld}, @@ -218,7 +218,7 @@ pub fn despawn() -> impl EntityCommand { /// An [`EntityCommand`] that creates an [`Observer`](crate::observer::Observer) /// listening for events of type `E` targeting an entity #[track_caller] -pub fn observe( +pub fn observe( observer: impl IntoObserverSystem, ) -> impl EntityCommand { let caller = MaybeLocation::caller(); @@ -227,11 +227,11 @@ pub fn observe( } } -/// An [`EntityCommand`] that sends an [observer](crate::observer::Observer) [`Event`] targeting an entity. +/// An [`EntityCommand`] that sends an [`EntityEvent`] targeting an entity. /// -/// This will run any [`Observer`](crate::observer::Observer) of the given [`Event`] watching the entity. +/// This will run any [`Observer`](crate::observer::Observer) of the given [`EntityEvent`] watching the entity. #[track_caller] -pub fn trigger(event: impl Event) -> impl EntityCommand { +pub fn trigger(event: impl EntityEvent) -> impl EntityCommand { let caller = MaybeLocation::caller(); move |mut entity: EntityWorldMut| { let id = entity.id(); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index d78836cc93..d36588d377 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -20,7 +20,7 @@ use crate::{ component::{Component, ComponentId, Mutable}, entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError}, error::{ignore, warn, BevyError, CommandWithEntity, ErrorContext, HandleError}, - event::Event, + event::{BufferedEvent, EntityEvent, Event}, observer::{Observer, TriggerTargets}, resource::Resource, schedule::ScheduleLabel, @@ -1078,7 +1078,7 @@ impl<'w, 's> Commands<'w, 's> { self.queue(command::run_system_cached_with(system, input).handle_error_with(warn)); } - /// Sends a global [observer](Observer) [`Event`] without any targets. + /// Sends a global [`Event`] without any targets. /// /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. #[track_caller] @@ -1086,13 +1086,13 @@ impl<'w, 's> Commands<'w, 's> { self.queue(command::trigger(event)); } - /// Sends an [observer](Observer) [`Event`] for the given targets. + /// Sends an [`EntityEvent`] for the given targets. /// - /// This will run any [`Observer`] of the given [`Event`] watching those targets. + /// This will run any [`Observer`] of the given [`EntityEvent`] watching those targets. #[track_caller] pub fn trigger_targets( &mut self, - event: impl Event, + event: impl EntityEvent, targets: impl TriggerTargets + Send + Sync + 'static, ) { self.queue(command::trigger_targets(event, targets)); @@ -1119,7 +1119,7 @@ impl<'w, 's> Commands<'w, 's> { self.spawn(Observer::new(observer)) } - /// Sends an arbitrary [`Event`]. + /// Sends an arbitrary [`BufferedEvent`]. /// /// This is a convenience method for sending events /// without requiring an [`EventWriter`](crate::event::EventWriter). @@ -1132,7 +1132,7 @@ impl<'w, 's> Commands<'w, 's> { /// If these events are performance-critical or very frequently sent, /// consider using a typed [`EventWriter`](crate::event::EventWriter) instead. #[track_caller] - pub fn send_event(&mut self, event: E) -> &mut Self { + pub fn send_event(&mut self, event: E) -> &mut Self { self.queue(command::send_event(event)); self } @@ -1957,16 +1957,16 @@ impl<'a> EntityCommands<'a> { &mut self.commands } - /// Sends an [observer](Observer) [`Event`] targeting the entity. + /// Sends an [`EntityEvent`] targeting the entity. /// - /// This will run any [`Observer`] of the given [`Event`] watching this entity. + /// This will run any [`Observer`] of the given [`EntityEvent`] watching this entity. #[track_caller] - pub fn trigger(&mut self, event: impl Event) -> &mut Self { + pub fn trigger(&mut self, event: impl EntityEvent) -> &mut Self { self.queue(entity_command::trigger(event)) } /// Creates an [`Observer`] listening for events of type `E` targeting this entity. - pub fn observe( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 13e531648d..4720860b23 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -131,7 +131,7 @@ impl SystemMeta { /// # use bevy_ecs::system::SystemState; /// # use bevy_ecs::event::Events; /// # -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// # #[derive(Resource)] /// # struct MyResource(u32); @@ -164,7 +164,7 @@ impl SystemMeta { /// # use bevy_ecs::system::SystemState; /// # use bevy_ecs::event::Events; /// # -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// #[derive(Resource)] /// struct CachedSystemState { diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 8f36e6a8c2..153f85f336 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -57,7 +57,7 @@ use variadics_please::{all_tuples, all_tuples_enumerated}; /// # use bevy_ecs::prelude::*; /// # #[derive(Resource)] /// # struct SomeResource; -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct SomeEvent; /// # #[derive(Resource)] /// # struct SomeOtherResource; @@ -598,7 +598,7 @@ unsafe impl<'w, 's, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> Re /// ``` /// # use bevy_ecs::prelude::*; /// # -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// # impl MyEvent { /// # pub fn new() -> Self { Self } @@ -2841,7 +2841,7 @@ impl Display for SystemParamValidationError { #[cfg(test)] mod tests { use super::*; - use crate::system::assert_is_system; + use crate::{event::Event, system::assert_is_system}; use core::cell::RefCell; // Compile test for https://github.com/bevyengine/bevy/pull/2838. @@ -3087,11 +3087,11 @@ mod tests { } #[test] - #[should_panic = "Encountered an error in system `bevy_ecs::system::system_param::tests::missing_event_error::event_system`: Parameter `EventReader::events` failed validation: Event not initialized"] + #[should_panic = "Encountered an error in system `bevy_ecs::system::system_param::tests::missing_event_error::event_system`: Parameter `EventReader::events` failed validation: BufferedEvent not initialized"] fn missing_event_error() { - use crate::prelude::{Event, EventReader}; + use crate::prelude::{BufferedEvent, EventReader}; - #[derive(Event)] + #[derive(Event, BufferedEvent)] pub struct MissingEvent; let mut schedule = crate::schedule::Schedule::default(); diff --git a/crates/bevy_ecs/src/traversal.rs b/crates/bevy_ecs/src/traversal.rs index 86772a3b18..d6527a10fa 100644 --- a/crates/bevy_ecs/src/traversal.rs +++ b/crates/bevy_ecs/src/traversal.rs @@ -17,7 +17,7 @@ use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship /// parameter `D` is the event type given in `On`. This allows traversal to differ depending on event /// data. /// -/// [specify the direction]: crate::event::Event::Traversal +/// [specify the direction]: crate::event::EntityEvent::Traversal /// [event propagation]: crate::observer::On::propagate /// [observers]: crate::observer::Observer pub trait Traversal: ReadOnlyQueryData { diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index acf48812c1..3e55dd8087 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -5,7 +5,7 @@ use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::{ComponentId, Mutable}, entity::Entity, - event::{Event, EventId, Events, SendBatchIds}, + event::{BufferedEvent, EntityEvent, Event, EventId, Events, SendBatchIds}, lifecycle::{HookContext, INSERT, REPLACE}, observer::{Observers, TriggerTargets}, prelude::{Component, QueryState}, @@ -496,27 +496,27 @@ impl<'w> DeferredWorld<'w> { unsafe { self.world.get_non_send_resource_mut() } } - /// Sends an [`Event`]. + /// Sends a [`BufferedEvent`]. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event(&mut self, event: E) -> Option> { + pub fn send_event(&mut self, event: E) -> Option> { self.send_event_batch(core::iter::once(event))?.next() } - /// Sends the default value of the [`Event`] of type `E`. + /// Sends the default value of the [`BufferedEvent`] of type `E`. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_default(&mut self) -> Option> { + pub fn send_event_default(&mut self) -> Option> { self.send_event(E::default()) } - /// Sends a batch of [`Event`]s from an iterator. + /// Sends a batch of [`BufferedEvent`]s from an iterator. /// This method returns the [IDs](`EventId`) of the sent `events`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_batch( + pub fn send_event_batch( &mut self, events: impl IntoIterator, ) -> Option> { @@ -807,15 +807,23 @@ impl<'w> DeferredWorld<'w> { } } - /// Sends a "global" [observer](crate::observer::Observer) [`Event`] without any targets. + /// Sends a global [`Event`] without any targets. + /// + /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. + /// + /// [`Observer`]: crate::observer::Observer pub fn trigger(&mut self, trigger: impl Event) { self.commands().trigger(trigger); } - /// Sends an [observer](crate::observer::Observer) [`Event`] with the given `targets`. + /// Sends an [`EntityEvent`] with the given `targets` + /// + /// This will run any [`Observer`] of the given [`EntityEvent`] watching those targets. + /// + /// [`Observer`]: crate::observer::Observer pub fn trigger_targets( &mut self, - trigger: impl Event, + trigger: impl EntityEvent, targets: impl TriggerTargets + Send + Sync + 'static, ) { self.commands().trigger_targets(trigger, targets); diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 44d4604331..ab470efcc7 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -13,7 +13,7 @@ use crate::{ ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, EntityIdLocation, EntityLocation, }, - event::Event, + event::EntityEvent, lifecycle::{DESPAWN, REMOVE, REPLACE}, observer::Observer, query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData}, @@ -2626,7 +2626,7 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. - pub fn trigger(&mut self, event: impl Event) -> &mut Self { + pub fn trigger(&mut self, event: impl EntityEvent) -> &mut Self { self.assert_not_despawned(); self.world.trigger_targets(event, self.entity); self.world.flush(); @@ -2643,14 +2643,14 @@ impl<'w> EntityWorldMut<'w> { /// /// Panics if the given system is an exclusive system. #[track_caller] - pub fn observe( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { self.observe_with_caller(observer, MaybeLocation::caller()) } - pub(crate) fn observe_with_caller( + pub(crate) fn observe_with_caller( &mut self, observer: impl IntoObserverSystem, caller: MaybeLocation, @@ -5739,7 +5739,7 @@ mod tests { assert_eq!((&mut X(8), &mut Y(9)), (x_component, y_component)); } - #[derive(Event)] + #[derive(Event, EntityEvent)] struct TestEvent; #[test] @@ -5748,9 +5748,7 @@ mod tests { let entity = world .spawn_empty() .observe(|trigger: On, mut commands: Commands| { - commands - .entity(trigger.target().unwrap()) - .insert(TestComponent(0)); + commands.entity(trigger.target()).insert(TestComponent(0)); }) .id(); @@ -5769,7 +5767,7 @@ mod tests { fn location_on_despawned_entity_panics() { let mut world = World::new(); world.add_observer(|trigger: On, mut commands: Commands| { - commands.entity(trigger.target().unwrap()).despawn(); + commands.entity(trigger.target()).despawn(); }); let entity = world.spawn_empty().id(); let mut a = world.entity_mut(entity); diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index dbc537fc8e..ed7c1f2cdd 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -19,6 +19,7 @@ pub use crate::{ }; use crate::{ error::{DefaultErrorHandler, ErrorHandler}, + event::BufferedEvent, lifecycle::{ComponentHooks, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, prelude::{Add, Despawn, Insert, Remove, Replace}, }; @@ -2598,27 +2599,27 @@ impl World { Some(result) } - /// Sends an [`Event`]. + /// Sends a [`BufferedEvent`]. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event(&mut self, event: E) -> Option> { + pub fn send_event(&mut self, event: E) -> Option> { self.send_event_batch(core::iter::once(event))?.next() } - /// Sends the default value of the [`Event`] of type `E`. + /// Sends the default value of the [`BufferedEvent`] of type `E`. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_default(&mut self) -> Option> { + pub fn send_event_default(&mut self) -> Option> { self.send_event(E::default()) } - /// Sends a batch of [`Event`]s from an iterator. + /// Sends a batch of [`BufferedEvent`]s from an iterator. /// This method returns the [IDs](`EventId`) of the sent `events`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_batch( + pub fn send_event_batch( &mut self, events: impl IntoIterator, ) -> Option> { diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs index 2b0148909c..7d2f551201 100644 --- a/crates/bevy_input/src/gamepad.rs +++ b/crates/bevy_input/src/gamepad.rs @@ -10,7 +10,7 @@ use bevy_ecs::{ change_detection::DetectChangesMut, component::Component, entity::Entity, - event::{Event, EventReader, EventWriter}, + event::{BufferedEvent, Event, EventReader, EventWriter}, name::Name, system::{Commands, Query}, }; @@ -32,7 +32,7 @@ use thiserror::Error; /// the in-frame relative ordering of events is important. /// /// This event is produced by `bevy_input`. -#[derive(Event, Debug, Clone, PartialEq, From)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, From)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -59,7 +59,7 @@ pub enum GamepadEvent { /// the in-frame relative ordering of events is important. /// /// This event type is used by `bevy_input` to feed its components. -#[derive(Event, Debug, Clone, PartialEq, From)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, From)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -80,7 +80,7 @@ pub enum RawGamepadEvent { } /// [`GamepadButton`] changed event unfiltered by [`GamepadSettings`]. -#[derive(Event, Debug, Copy, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Copy, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -112,7 +112,7 @@ impl RawGamepadButtonChangedEvent { } /// [`GamepadAxis`] changed event unfiltered by [`GamepadSettings`]. -#[derive(Event, Debug, Copy, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Copy, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -143,9 +143,9 @@ impl RawGamepadAxisChangedEvent { } } -/// A Gamepad connection event. Created when a connection to a gamepad +/// A [`Gamepad`] connection event. Created when a connection to a gamepad /// is established and when a gamepad is disconnected. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -184,7 +184,7 @@ impl GamepadConnectionEvent { } /// [`GamepadButton`] event triggered by a digital state change. -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -216,7 +216,7 @@ impl GamepadButtonStateChangedEvent { } /// [`GamepadButton`] event triggered by an analog state change. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -251,7 +251,7 @@ impl GamepadButtonChangedEvent { } /// [`GamepadAxis`] event triggered by an analog state change. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( feature = "bevy_reflect", @@ -1774,7 +1774,7 @@ impl GamepadRumbleIntensity { #[doc(alias = "force feedback")] #[doc(alias = "vibration")] #[doc(alias = "vibrate")] -#[derive(Event, Clone)] +#[derive(Event, BufferedEvent, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Clone))] pub enum GamepadRumbleRequest { /// Add a rumble to the given gamepad. diff --git a/crates/bevy_input/src/gestures.rs b/crates/bevy_input/src/gestures.rs index 5cd14d4634..9daa21d525 100644 --- a/crates/bevy_input/src/gestures.rs +++ b/crates/bevy_input/src/gestures.rs @@ -1,6 +1,6 @@ //! Gestures functionality, from touchscreens and touchpads. -use bevy_ecs::event::Event; +use bevy_ecs::event::{BufferedEvent, Event}; use bevy_math::Vec2; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -17,7 +17,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// - Only available on **`macOS`** and **`iOS`**. /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -39,7 +39,7 @@ pub struct PinchGesture(pub f32); /// /// - Only available on **`macOS`** and **`iOS`**. /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -58,7 +58,7 @@ pub struct RotationGesture(pub f32); /// /// - Only available on **`macOS`** and **`iOS`**. /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -76,7 +76,7 @@ pub struct DoubleTapGesture; /// ## Platform-specific /// /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_input/src/keyboard.rs b/crates/bevy_input/src/keyboard.rs index ea5452fb53..909880ac7a 100644 --- a/crates/bevy_input/src/keyboard.rs +++ b/crates/bevy_input/src/keyboard.rs @@ -69,7 +69,7 @@ use crate::{ButtonInput, ButtonState}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::Entity, - event::{Event, EventReader}, + event::{BufferedEvent, Event, EventReader}, system::ResMut, }; @@ -94,7 +94,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// The event is consumed inside of the [`keyboard_input_system`] /// to update the [`ButtonInput`](ButtonInput) resource. -#[derive(Event, Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -139,7 +139,7 @@ pub struct KeyboardInput { /// when, for example, switching between windows with 'Alt-Tab' or using any other /// OS specific key combination that leads to Bevy window losing focus and not receiving any /// input events -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Clone, PartialEq))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( diff --git a/crates/bevy_input/src/mouse.rs b/crates/bevy_input/src/mouse.rs index 3a377d9329..e6b52bf51d 100644 --- a/crates/bevy_input/src/mouse.rs +++ b/crates/bevy_input/src/mouse.rs @@ -4,7 +4,7 @@ use crate::{ButtonInput, ButtonState}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::Entity, - event::{Event, EventReader}, + event::{BufferedEvent, Event, EventReader}, resource::Resource, system::ResMut, }; @@ -26,7 +26,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// The event is read inside of the [`mouse_button_input_system`] /// to update the [`ButtonInput`] resource. -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -91,7 +91,7 @@ pub enum MouseButton { /// However, the event data does not make it possible to distinguish which device it is referring to. /// /// [`DeviceEvent::MouseMotion`]: https://docs.rs/winit/latest/winit/event/enum.DeviceEvent.html#variant.MouseMotion -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -140,7 +140,7 @@ pub enum MouseScrollUnit { /// A mouse wheel event. /// /// This event is the translated version of the `WindowEvent::MouseWheel` from the `winit` crate. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_input/src/touch.rs b/crates/bevy_input/src/touch.rs index 28f3159d53..df1cf3764f 100644 --- a/crates/bevy_input/src/touch.rs +++ b/crates/bevy_input/src/touch.rs @@ -2,7 +2,7 @@ use bevy_ecs::{ entity::Entity, - event::{Event, EventReader}, + event::{BufferedEvent, Event, EventReader}, resource::Resource, system::ResMut, }; @@ -37,7 +37,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// This event is the translated version of the `WindowEvent::Touch` from the `winit` crate. /// It is available to the end user and can be used for game logic. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index d146b1cc56..1fe7c4b7ef 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -137,21 +137,16 @@ pub struct InputFocusVisible(pub bool); /// /// To set up your own bubbling input event, add the [`dispatch_focused_input::`](dispatch_focused_input) system to your app, /// in the [`InputFocusSystems::Dispatch`] system set during [`PreUpdate`]. -#[derive(Clone, Debug, Component)] +#[derive(Event, EntityEvent, Clone, Debug, Component)] +#[entity_event(traversal = WindowTraversal, auto_propagate)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))] -pub struct FocusedInput { +pub struct FocusedInput { /// The underlying input event. pub input: E, /// The primary window entity. window: Entity, } -impl Event for FocusedInput { - type Traversal = WindowTraversal; - - const AUTO_PROPAGATE: bool = true; -} - #[derive(QueryData)] /// These are for accessing components defined on the targeted entity pub struct WindowTraversal { @@ -159,7 +154,7 @@ pub struct WindowTraversal { window: Option<&'static Window>, } -impl Traversal> for WindowTraversal { +impl Traversal> for WindowTraversal { fn traverse(item: Self::Item<'_>, event: &FocusedInput) -> Option { let WindowTraversalItem { child_of, window } = item; @@ -230,7 +225,7 @@ pub fn set_initial_focus( /// System which dispatches bubbled input events to the focused entity, or to the primary window /// if no entity has focus. -pub fn dispatch_focused_input( +pub fn dispatch_focused_input( mut key_events: EventReader, focus: Res, windows: Query>, @@ -384,7 +379,7 @@ mod tests { trigger: On>, mut query: Query<&mut GatherKeyboardEvents>, ) { - if let Ok(mut gather) = query.get_mut(trigger.target().unwrap()) { + if let Ok(mut gather) = query.get_mut(trigger.target()) { if let Key::Character(c) = &trigger.input.logical_key { gather.0.push_str(c.as_str()); } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 7cd0287d81..83d28a7da7 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -551,7 +551,7 @@ pub(crate) fn add_light_view_entities( trigger: On, mut commands: Commands, ) { - if let Ok(mut v) = commands.get_entity(trigger.target().unwrap()) { + if let Ok(mut v) = commands.get_entity(trigger.target()) { v.insert(LightViewEntities::default()); } } @@ -561,7 +561,7 @@ pub(crate) fn extracted_light_removed( trigger: On, mut commands: Commands, ) { - if let Ok(mut v) = commands.get_entity(trigger.target().unwrap()) { + if let Ok(mut v) = commands.get_entity(trigger.target()) { v.try_remove::(); } } @@ -571,7 +571,7 @@ pub(crate) fn remove_light_view_entities( query: Query<&LightViewEntities>, mut commands: Commands, ) { - if let Ok(entities) = query.get(trigger.target().unwrap()) { + if let Ok(entities) = query.get(trigger.target()) { for v in entities.0.values() { for e in v.iter().copied() { if let Ok(mut v) = commands.get_entity(e) { diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 9e28cc6d7c..28693314d9 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -55,7 +55,7 @@ pub mod prelude { /// Note that systems reading these events in [`PreUpdate`](bevy_app::PreUpdate) will not report ordering /// ambiguities with picking backends. Take care to ensure such systems are explicitly ordered /// against [`PickingSystems::Backend`](crate::PickingSystems::Backend), or better, avoid reading `PointerHits` in `PreUpdate`. -#[derive(Event, Debug, Clone, Reflect)] +#[derive(Event, BufferedEvent, Debug, Clone, Reflect)] #[reflect(Debug, Clone)] pub struct PointerHits { /// The pointer associated with this hit test. diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 61f9da041d..393e4a9edc 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -59,7 +59,8 @@ use crate::{ /// /// The documentation for the [`pointer_events`] explains the events this module exposes and /// the order in which they fire. -#[derive(Clone, PartialEq, Debug, Reflect, Component)] +#[derive(Event, BufferedEvent, EntityEvent, Clone, PartialEq, Debug, Reflect, Component)] +#[entity_event(traversal = PointerTraversal, auto_propagate)] #[reflect(Component, Debug, Clone)] pub struct Pointer { /// The original target of this picking event, before bubbling @@ -106,15 +107,6 @@ where } } -impl Event for Pointer -where - E: Debug + Clone + Reflect, -{ - type Traversal = PointerTraversal; - - const AUTO_PROPAGATE: bool = true; -} - impl core::fmt::Display for Pointer { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!( diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index a54ecb61d9..74a765fbcd 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -48,20 +48,20 @@ //! # use bevy_ecs::prelude::*; //! # use bevy_transform::prelude::*; //! # use bevy_picking::prelude::*; -//! # #[derive(Event)] +//! # #[derive(Event, BufferedEvent)] //! # struct Greeting; //! fn setup(mut commands: Commands) { //! commands.spawn(Transform::default()) //! // Spawn your entity here, e.g. a `Mesh3d`. //! // When dragged, mutate the `Transform` component on the dragged target entity: //! .observe(|trigger: On>, mut transforms: Query<&mut Transform>| { -//! let mut transform = transforms.get_mut(trigger.target().unwrap()).unwrap(); +//! let mut transform = transforms.get_mut(trigger.target()).unwrap(); //! let drag = trigger.event(); //! transform.rotate_local_y(drag.delta.x / 50.0); //! }) //! .observe(|trigger: On>, mut commands: Commands| { -//! println!("Entity {} goes BOOM!", trigger.target().unwrap()); -//! commands.entity(trigger.target().unwrap()).despawn(); +//! println!("Entity {} goes BOOM!", trigger.target()); +//! commands.entity(trigger.target()).despawn(); //! }) //! .observe(|trigger: On>, mut events: EventWriter| { //! events.write(Greeting); diff --git a/crates/bevy_picking/src/pointer.rs b/crates/bevy_picking/src/pointer.rs index faba90cbb9..0406cb61f5 100644 --- a/crates/bevy_picking/src/pointer.rs +++ b/crates/bevy_picking/src/pointer.rs @@ -269,7 +269,7 @@ pub enum PointerAction { } /// An input event effecting a pointer. -#[derive(Event, Debug, Clone, Reflect)] +#[derive(Event, BufferedEvent, Debug, Clone, Reflect)] #[reflect(Clone)] pub struct PointerInput { /// The id of the pointer. diff --git a/crates/bevy_render/src/gpu_readback.rs b/crates/bevy_render/src/gpu_readback.rs index c05861f3da..adcb883559 100644 --- a/crates/bevy_render/src/gpu_readback.rs +++ b/crates/bevy_render/src/gpu_readback.rs @@ -15,14 +15,14 @@ use async_channel::{Receiver, Sender}; use bevy_app::{App, Plugin}; use bevy_asset::Handle; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::schedule::IntoScheduleConfigs; use bevy_ecs::{ change_detection::ResMut, entity::Entity, - event::Event, + event::EntityEvent, prelude::{Component, Resource, World}, system::{Query, Res}, }; +use bevy_ecs::{event::Event, schedule::IntoScheduleConfigs}; use bevy_image::{Image, TextureFormatPixelInfo}; use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; @@ -96,7 +96,7 @@ impl Readback { /// /// The event contains the data as a `Vec`, which can be interpreted as the raw bytes of the /// requested buffer or texture. -#[derive(Event, Deref, DerefMut, Reflect, Debug)] +#[derive(Event, EntityEvent, Deref, DerefMut, Reflect, Debug)] #[reflect(Debug)] pub struct ReadbackComplete(pub Vec); diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index ace2a97bf8..6dceaba3c0 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -95,14 +95,14 @@ impl Plugin for SyncWorldPlugin { app.init_resource::(); app.add_observer( |trigger: On, mut pending: ResMut| { - pending.push(EntityRecord::Added(trigger.target().unwrap())); + pending.push(EntityRecord::Added(trigger.target())); }, ); app.add_observer( |trigger: On, mut pending: ResMut, query: Query<&RenderEntity>| { - if let Ok(e) = query.get(trigger.target().unwrap()) { + if let Ok(e) = query.get(trigger.target()) { pending.push(EntityRecord::Removed(*e)); }; }, @@ -514,14 +514,14 @@ mod tests { main_world.add_observer( |trigger: On, mut pending: ResMut| { - pending.push(EntityRecord::Added(trigger.target().unwrap())); + pending.push(EntityRecord::Added(trigger.target())); }, ); main_world.add_observer( |trigger: On, mut pending: ResMut, query: Query<&RenderEntity>| { - if let Ok(e) = query.get(trigger.target().unwrap()) { + if let Ok(e) = query.get(trigger.target()) { pending.push(EntityRecord::Removed(*e)); }; }, diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index f7699e1648..33b76d269d 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -39,7 +39,7 @@ use std::{ use tracing::{error, info, warn}; use wgpu::{CommandEncoder, Extent3d, TextureFormat}; -#[derive(Event, Deref, DerefMut, Reflect, Debug)] +#[derive(Event, EntityEvent, Deref, DerefMut, Reflect, Debug)] #[reflect(Debug)] pub struct ScreenshotCaptured(pub Image); diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 4e17af510f..a15d00f116 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -2,7 +2,7 @@ use crate::{DynamicScene, Scene}; use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_ecs::{ entity::{Entity, EntityHashMap}, - event::{Event, EventCursor, Events}, + event::{EntityEvent, Event, EventCursor, Events}, hierarchy::ChildOf, reflect::AppTypeRegistry, resource::Resource, @@ -25,7 +25,7 @@ use bevy_ecs::{ /// See also [`On`], [`SceneSpawner::instance_is_ready`]. /// /// [`On`]: bevy_ecs::observer::On -#[derive(Clone, Copy, Debug, Eq, PartialEq, Event, Reflect)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Event, EntityEvent, Reflect)] #[reflect(Debug, PartialEq, Clone)] pub struct SceneInstanceReady { /// Instance which has been spawned. @@ -734,7 +734,7 @@ mod tests { ); assert_eq!( trigger.target(), - scene_entity, + scene_entity.unwrap_or(Entity::PLACEHOLDER), "`SceneInstanceReady` triggered on the wrong parent entity" ); assert!( diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs index dfe711f245..1ee21826c3 100644 --- a/crates/bevy_state/src/state/transitions.rs +++ b/crates/bevy_state/src/state/transitions.rs @@ -1,7 +1,7 @@ use core::{marker::PhantomData, mem}; use bevy_ecs::{ - event::{Event, EventReader, EventWriter}, + event::{BufferedEvent, Event, EventReader, EventWriter}, schedule::{IntoScheduleConfigs, Schedule, ScheduleLabel, Schedules, SystemSet}, system::{Commands, In, ResMut}, world::World, @@ -55,11 +55,11 @@ pub struct OnTransition { #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)] pub struct StateTransition; -/// Event sent when any state transition of `S` happens. +/// A [`BufferedEvent`] sent when any state transition of `S` happens. /// This includes identity transitions, where `exited` and `entered` have the same value. /// /// If you know exactly what state you want to respond to ahead of time, consider [`OnEnter`], [`OnTransition`], or [`OnExit`] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Event)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Event, BufferedEvent)] pub struct StateTransitionEvent { /// The state being exited. pub exited: Option, diff --git a/crates/bevy_state/src/state_scoped_events.rs b/crates/bevy_state/src/state_scoped_events.rs index b11d8e79df..adba1ca6b6 100644 --- a/crates/bevy_state/src/state_scoped_events.rs +++ b/crates/bevy_state/src/state_scoped_events.rs @@ -3,7 +3,7 @@ use core::marker::PhantomData; use bevy_app::{App, SubApp}; use bevy_ecs::{ - event::{Event, EventReader, Events}, + event::{BufferedEvent, EventReader, Events}, resource::Resource, system::Commands, world::World, @@ -12,7 +12,7 @@ use bevy_platform::collections::HashMap; use crate::state::{OnEnter, OnExit, StateTransitionEvent, States}; -fn clear_event_queue(w: &mut World) { +fn clear_event_queue(w: &mut World) { if let Some(mut queue) = w.get_resource_mut::>() { queue.clear(); } @@ -33,7 +33,7 @@ struct StateScopedEvents { } impl StateScopedEvents { - fn add_event(&mut self, state: S, transition_type: TransitionType) { + fn add_event(&mut self, state: S, transition_type: TransitionType) { let map = match transition_type { TransitionType::OnExit => &mut self.on_exit, TransitionType::OnEnter => &mut self.on_enter, @@ -106,7 +106,7 @@ fn clear_events_on_enter_state( }); } -fn clear_events_on_state_transition( +fn clear_events_on_state_transition( app: &mut SubApp, _p: PhantomData, state: S, @@ -128,7 +128,7 @@ fn clear_events_on_state_transition( /// Extension trait for [`App`] adding methods for registering state scoped events. pub trait StateScopedEventsAppExt { - /// Clears an [`Event`] when exiting the specified `state`. + /// Clears an [`BufferedEvent`] when exiting the specified `state`. /// /// Note that event cleanup is ambiguously ordered relative to /// [`DespawnOnExitState`](crate::prelude::DespawnOnExitState) entity cleanup, @@ -136,9 +136,9 @@ pub trait StateScopedEventsAppExt { /// All of these (state scoped entities and events cleanup, and `OnExit`) /// occur within schedule [`StateTransition`](crate::prelude::StateTransition) /// and system set `StateTransitionSystems::ExitSchedules`. - fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self; + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self; - /// Clears an [`Event`] when entering the specified `state`. + /// Clears an [`BufferedEvent`] when entering the specified `state`. /// /// Note that event cleanup is ambiguously ordered relative to /// [`DespawnOnEnterState`](crate::prelude::DespawnOnEnterState) entity cleanup, @@ -146,11 +146,11 @@ pub trait StateScopedEventsAppExt { /// All of these (state scoped entities and events cleanup, and `OnEnter`) /// occur within schedule [`StateTransition`](crate::prelude::StateTransition) /// and system set `StateTransitionSystems::EnterSchedules`. - fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self; + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self; } impl StateScopedEventsAppExt for App { - fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition( self.main_mut(), PhantomData::, @@ -160,7 +160,7 @@ impl StateScopedEventsAppExt for App { self } - fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition( self.main_mut(), PhantomData::, @@ -172,12 +172,12 @@ impl StateScopedEventsAppExt for App { } impl StateScopedEventsAppExt for SubApp { - fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition(self, PhantomData::, state, TransitionType::OnExit); self } - fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition(self, PhantomData::, state, TransitionType::OnEnter); self } @@ -187,6 +187,7 @@ impl StateScopedEventsAppExt for SubApp { mod tests { use super::*; use crate::app::StatesPlugin; + use bevy_ecs::event::{BufferedEvent, Event}; use bevy_state::prelude::*; #[derive(States, Default, Clone, Hash, Eq, PartialEq, Debug)] @@ -196,10 +197,10 @@ mod tests { B, } - #[derive(Event, Debug)] + #[derive(Event, BufferedEvent, Debug)] struct StandardEvent; - #[derive(Event, Debug)] + #[derive(Event, BufferedEvent, Debug)] struct StateScopedEvent; #[test] diff --git a/crates/bevy_time/src/lib.rs b/crates/bevy_time/src/lib.rs index 6e7b3b3991..c4118b876c 100644 --- a/crates/bevy_time/src/lib.rs +++ b/crates/bevy_time/src/lib.rs @@ -185,7 +185,10 @@ mod tests { use crate::{Fixed, Time, TimePlugin, TimeUpdateStrategy, Virtual}; use bevy_app::{App, FixedUpdate, Startup, Update}; use bevy_ecs::{ - event::{Event, EventReader, EventRegistry, EventWriter, Events, ShouldUpdateEvents}, + event::{ + BufferedEvent, Event, EventReader, EventRegistry, EventWriter, Events, + ShouldUpdateEvents, + }, resource::Resource, system::{Local, Res, ResMut}, }; @@ -193,7 +196,7 @@ mod tests { use core::time::Duration; use std::println; - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct TestEvent { sender: std::sync::mpsc::Sender, } @@ -206,7 +209,7 @@ mod tests { } } - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct DummyEvent; #[derive(Resource, Default)] diff --git a/crates/bevy_time/src/time.rs b/crates/bevy_time/src/time.rs index d0845e22d3..8c4456f0e3 100644 --- a/crates/bevy_time/src/time.rs +++ b/crates/bevy_time/src/time.rs @@ -119,7 +119,7 @@ use { /// # use bevy_ecs::prelude::*; /// # use bevy_time::prelude::*; /// # -/// #[derive(Event)] +/// #[derive(Event, BufferedEvent)] /// struct PauseEvent(bool); /// /// fn pause_system(mut time: ResMut>, mut events: EventReader) { diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs index 04d5ba041d..6659204da4 100644 --- a/crates/bevy_ui/src/interaction_states.rs +++ b/crates/bevy_ui/src/interaction_states.rs @@ -19,7 +19,7 @@ use bevy_ecs::{ pub struct InteractionDisabled; pub(crate) fn on_add_disabled(trigger: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(trigger.target().unwrap()); + let mut entity = world.entity_mut(trigger.target()); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_disabled(); } @@ -29,7 +29,7 @@ pub(crate) fn on_remove_disabled( trigger: On, mut world: DeferredWorld, ) { - let mut entity = world.entity_mut(trigger.target().unwrap()); + let mut entity = world.entity_mut(trigger.target()); if let Some(mut accessibility) = entity.get_mut::() { accessibility.clear_disabled(); } @@ -53,7 +53,7 @@ impl Checked { } pub(crate) fn on_insert_is_checked(trigger: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(trigger.target().unwrap()); + let mut entity = world.entity_mut(trigger.target()); let checked = entity.get::().unwrap().get(); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_toggled(match checked { @@ -64,7 +64,7 @@ pub(crate) fn on_insert_is_checked(trigger: On, mut world: Defe } pub(crate) fn on_remove_is_checked(trigger: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(trigger.target().unwrap()); + let mut entity = world.entity_mut(trigger.target()); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_toggled(accesskit::Toggled::False); } diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 026b85dc32..5a320439d7 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -1,5 +1,8 @@ use alloc::string::String; -use bevy_ecs::{entity::Entity, event::Event}; +use bevy_ecs::{ + entity::Entity, + event::{BufferedEvent, Event}, +}; use bevy_input::{ gestures::*, keyboard::{KeyboardFocusLost, KeyboardInput}, @@ -23,7 +26,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use crate::WindowTheme; /// A window event that is sent whenever a window's logical size has changed. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -45,7 +48,7 @@ pub struct WindowResized { /// An event that indicates all of the application's windows should be redrawn, /// even if their control flow is set to `Wait` and there have been no window events. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -61,7 +64,7 @@ pub struct RequestRedraw; /// An event that is sent whenever a new window is created. /// /// To create a new window, spawn an entity with a [`crate::Window`] on it. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -87,7 +90,7 @@ pub struct WindowCreated { /// /// [`WindowPlugin`]: crate::WindowPlugin /// [`Window`]: crate::Window -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -105,7 +108,7 @@ pub struct WindowCloseRequested { /// An event that is sent whenever a window is closed. This will be sent when /// the window entity loses its [`Window`](crate::window::Window) component or is despawned. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -126,7 +129,7 @@ pub struct WindowClosed { /// An event that is sent whenever a window is closing. This will be sent when /// after a [`WindowCloseRequested`] event is received and the window is in the process of closing. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -146,7 +149,7 @@ pub struct WindowClosing { /// /// Note that if your application only has a single window, this event may be your last chance to /// persist state before the application terminates. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -176,7 +179,7 @@ pub struct WindowDestroyed { /// you should not use it for non-cursor-like behavior such as 3D camera control. Please see `MouseMotion` instead. /// /// [`WindowEvent::CursorMoved`]: https://docs.rs/winit/latest/winit/event/enum.WindowEvent.html#variant.CursorMoved -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -201,7 +204,7 @@ pub struct CursorMoved { } /// An event that is sent whenever the user's cursor enters a window. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -218,7 +221,7 @@ pub struct CursorEntered { } /// An event that is sent whenever the user's cursor leaves a window. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -239,7 +242,7 @@ pub struct CursorLeft { /// This event is the translated version of the `WindowEvent::Ime` from the `winit` crate. /// /// It is only sent if IME was enabled on the window with [`Window::ime_enabled`](crate::window::Window::ime_enabled). -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -284,7 +287,7 @@ pub enum Ime { } /// An event that indicates a window has received or lost focus. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -311,7 +314,7 @@ pub struct WindowFocused { /// It is the translated version of [`WindowEvent::Occluded`] from the `winit` crate. /// /// [`WindowEvent::Occluded`]: https://docs.rs/winit/latest/winit/event/enum.WindowEvent.html#variant.Occluded -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -330,7 +333,7 @@ pub struct WindowOccluded { } /// An event that indicates a window's scale factor has changed. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -349,7 +352,7 @@ pub struct WindowScaleFactorChanged { } /// An event that indicates a window's OS-reported scale factor has changed. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -368,7 +371,7 @@ pub struct WindowBackendScaleFactorChanged { } /// Events related to files being dragged and dropped on a window. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -404,7 +407,7 @@ pub enum FileDragAndDrop { } /// An event that is sent when a window is repositioned in physical pixels. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -426,7 +429,7 @@ pub struct WindowMoved { /// /// This event is only sent when the window is relying on the system theme to control its appearance. /// i.e. It is only sent when [`Window::window_theme`](crate::window::Window::window_theme) is `None` and the system theme changes. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -445,7 +448,7 @@ pub struct WindowThemeChanged { } /// Application lifetime events -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -488,7 +491,7 @@ impl AppLifecycle { /// access window events in the order they were received from the /// operating system. Otherwise, the event types are individually /// readable with `EventReader` (e.g. `EventReader`). -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_winit/src/cursor.rs b/crates/bevy_winit/src/cursor.rs index 580c7ebee4..c5c5e489a6 100644 --- a/crates/bevy_winit/src/cursor.rs +++ b/crates/bevy_winit/src/cursor.rs @@ -195,7 +195,7 @@ fn update_cursors( fn on_remove_cursor_icon(trigger: On, mut commands: Commands) { // Use `try_insert` to avoid panic if the window is being destroyed. commands - .entity(trigger.target().unwrap()) + .entity(trigger.target()) .try_insert(PendingCursor(Some(CursorSource::System( convert_system_cursor_icon(SystemCursorIcon::Default), )))); diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 4373c4dcfd..6d814d4ac2 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -71,9 +71,9 @@ thread_local! { /// in systems. /// /// When using eg. `MinimalPlugins` you can add this using `WinitPlugin::::default()`, where -/// `WakeUp` is the default `Event` that bevy uses. +/// `WakeUp` is the default event that bevy uses. #[derive(Default)] -pub struct WinitPlugin { +pub struct WinitPlugin { /// Allows the window (and the event loop) to be created on any thread /// instead of only the main thread. /// @@ -87,7 +87,7 @@ pub struct WinitPlugin { marker: PhantomData, } -impl Plugin for WinitPlugin { +impl Plugin for WinitPlugin { fn name(&self) -> &str { "bevy_winit::WinitPlugin" } @@ -155,7 +155,7 @@ impl Plugin for WinitPlugin { /// The default event that can be used to wake the window loop /// Wakes up the loop if in wait state -#[derive(Debug, Default, Clone, Copy, Event, Reflect)] +#[derive(Debug, Default, Clone, Copy, Event, BufferedEvent, Reflect)] #[reflect(Debug, Default, Clone)] pub struct WakeUp; @@ -166,7 +166,7 @@ pub struct WakeUp; /// /// When you receive this event it has already been handled by Bevy's main loop. /// Sending these events will NOT cause them to be processed by Bevy. -#[derive(Debug, Clone, Event)] +#[derive(Debug, Clone, Event, BufferedEvent)] pub struct RawWinitWindowEvent { /// The window for which the event was fired. pub window_id: WindowId, diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index 083341fd2b..934de5dad7 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -58,7 +58,7 @@ use crate::{ /// Persistent state that is used to run the [`App`] according to the current /// [`UpdateMode`]. -struct WinitAppRunnerState { +struct WinitAppRunnerState { /// The running app. app: App, /// Exit value once the loop is finished. @@ -106,7 +106,7 @@ struct WinitAppRunnerState { )>, } -impl WinitAppRunnerState { +impl WinitAppRunnerState { fn new(mut app: App) -> Self { app.add_event::(); #[cfg(feature = "custom_cursor")] @@ -198,7 +198,7 @@ pub enum CursorSource { #[derive(Component, Debug)] pub struct PendingCursor(pub Option); -impl ApplicationHandler for WinitAppRunnerState { +impl ApplicationHandler for WinitAppRunnerState { fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { if event_loop.exiting() { return; @@ -549,7 +549,7 @@ impl ApplicationHandler for WinitAppRunnerState { } } -impl WinitAppRunnerState { +impl WinitAppRunnerState { fn redraw_requested(&mut self, event_loop: &ActiveEventLoop) { let mut redraw_event_reader = EventCursor::::default(); @@ -934,7 +934,7 @@ impl WinitAppRunnerState { /// /// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the /// `EventLoop`. -pub fn winit_runner(mut app: App, event_loop: EventLoop) -> AppExit { +pub fn winit_runner(mut app: App, event_loop: EventLoop) -> AppExit { if app.plugins_state() == PluginsState::Ready { app.finish(); app.cleanup(); diff --git a/examples/3d/edit_material_on_gltf.rs b/examples/3d/edit_material_on_gltf.rs index 11bd35bd1f..97c3c48296 100644 --- a/examples/3d/edit_material_on_gltf.rs +++ b/examples/3d/edit_material_on_gltf.rs @@ -65,12 +65,12 @@ fn change_material( mut asset_materials: ResMut>, ) { // Get the `ColorOverride` of the entity, if it does not have a color override, skip - let Ok(color_override) = color_override.get(trigger.target().unwrap()) else { + let Ok(color_override) = color_override.get(trigger.target()) else { return; }; // Iterate over all children recursively - for descendants in children.iter_descendants(trigger.target().unwrap()) { + for descendants in children.iter_descendants(trigger.target()) { // Get the material of the descendant if let Some(material) = mesh_materials .get(descendants) diff --git a/examples/3d/mixed_lighting.rs b/examples/3d/mixed_lighting.rs index a32efe0a86..ffff8652b4 100644 --- a/examples/3d/mixed_lighting.rs +++ b/examples/3d/mixed_lighting.rs @@ -72,7 +72,7 @@ enum LightingMode { /// An event that's fired whenever the user changes the lighting mode. /// /// This is also fired when the scene loads for the first time. -#[derive(Clone, Copy, Default, Event)] +#[derive(Clone, Copy, Default, Event, BufferedEvent)] struct LightingModeChanged; #[derive(Clone, Copy, Component, Debug)] diff --git a/examples/3d/solari.rs b/examples/3d/solari.rs index 37f78a26ec..389272cbb1 100644 --- a/examples/3d/solari.rs +++ b/examples/3d/solari.rs @@ -74,7 +74,7 @@ fn add_raytracing_meshes_on_scene_load( } } - for descendant in children.iter_descendants(trigger.target().unwrap()) { + for descendant in children.iter_descendants(trigger.target()) { if let Ok(mesh) = mesh.get(descendant) { commands .entity(descendant) diff --git a/examples/animation/animated_mesh.rs b/examples/animation/animated_mesh.rs index c79b9ca79e..06e6c45a58 100644 --- a/examples/animation/animated_mesh.rs +++ b/examples/animation/animated_mesh.rs @@ -70,12 +70,12 @@ fn play_animation_when_ready( ) { // The entity we spawned in `setup_mesh_and_animation` is the trigger's target. // Start by finding the AnimationToPlay component we added to that entity. - if let Ok(animation_to_play) = animations_to_play.get(trigger.target().unwrap()) { + if let Ok(animation_to_play) = animations_to_play.get(trigger.target()) { // The SceneRoot component will have spawned the scene as a hierarchy // of entities parented to our entity. Since the asset contained a skinned // mesh and animations, it will also have spawned an animation player // component. Search our entity's descendants to find the animation player. - for child in children.iter_descendants(trigger.target().unwrap()) { + for child in children.iter_descendants(trigger.target()) { if let Ok(mut player) = players.get_mut(child) { // Tell the animation player to start the animation and keep // repeating it. diff --git a/examples/animation/animated_mesh_events.rs b/examples/animation/animated_mesh_events.rs index f5e6b4dea4..b9ce729bad 100644 --- a/examples/animation/animated_mesh_events.rs +++ b/examples/animation/animated_mesh_events.rs @@ -37,7 +37,7 @@ struct Animations { graph_handle: Handle, } -#[derive(Event, Reflect, Clone)] +#[derive(Event, EntityEvent, Reflect, Clone)] struct OnStep; fn observe_on_step( @@ -47,10 +47,7 @@ fn observe_on_step( transforms: Query<&GlobalTransform>, mut seeded_rng: ResMut, ) { - let translation = transforms - .get(trigger.target().unwrap()) - .unwrap() - .translation(); + let translation = transforms.get(trigger.target()).unwrap().translation(); // Spawn a bunch of particles. for _ in 0..14 { let horizontal = seeded_rng.0.r#gen::() * seeded_rng.0.gen_range(8.0..12.0); diff --git a/examples/animation/animation_events.rs b/examples/animation/animation_events.rs index d31068911b..5c55691070 100644 --- a/examples/animation/animation_events.rs +++ b/examples/animation/animation_events.rs @@ -9,7 +9,6 @@ use bevy::{ fn main() { App::new() .add_plugins(DefaultPlugins) - .add_event::() .add_systems(Startup, setup) .add_systems(Update, animate_text_opacity) .add_observer(edit_message) @@ -19,7 +18,7 @@ fn main() { #[derive(Component)] struct MessageText; -#[derive(Event, Clone)] +#[derive(Event, EntityEvent, Clone)] struct MessageEvent { value: String, color: Color, diff --git a/examples/app/log_layers_ecs.rs b/examples/app/log_layers_ecs.rs index 059ffcbe20..1ee988b7cf 100644 --- a/examples/app/log_layers_ecs.rs +++ b/examples/app/log_layers_ecs.rs @@ -38,7 +38,7 @@ fn main() { } /// A basic message. This is what we will be sending from the [`CaptureLayer`] to [`CapturedLogEvents`] non-send resource. -#[derive(Debug, Event)] +#[derive(Debug, Event, BufferedEvent)] struct LogEvent { message: String, level: Level, diff --git a/examples/async_tasks/external_source_external_thread.rs b/examples/async_tasks/external_source_external_thread.rs index 1b437ed76c..9cd7a57ae4 100644 --- a/examples/async_tasks/external_source_external_thread.rs +++ b/examples/async_tasks/external_source_external_thread.rs @@ -20,7 +20,7 @@ fn main() { #[derive(Resource, Deref)] struct StreamReceiver(Receiver); -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct StreamEvent(u32); fn setup(mut commands: Commands) { diff --git a/examples/audio/pitch.rs b/examples/audio/pitch.rs index 4a49a8885f..6f4108025a 100644 --- a/examples/audio/pitch.rs +++ b/examples/audio/pitch.rs @@ -12,7 +12,7 @@ fn main() { .run(); } -#[derive(Event, Default)] +#[derive(Event, BufferedEvent, Default)] struct PlayPitch; #[derive(Resource)] diff --git a/examples/ecs/component_hooks.rs b/examples/ecs/component_hooks.rs index 4ce6b7b09b..7a97bf454c 100644 --- a/examples/ecs/component_hooks.rs +++ b/examples/ecs/component_hooks.rs @@ -45,7 +45,7 @@ impl Component for MyComponent { #[derive(Resource, Default, Debug, Deref, DerefMut)] struct MyComponentIndex(HashMap); -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct MyEvent; fn main() { diff --git a/examples/ecs/entity_disabling.rs b/examples/ecs/entity_disabling.rs index 66de6c74f4..362b655dfb 100644 --- a/examples/ecs/entity_disabling.rs +++ b/examples/ecs/entity_disabling.rs @@ -40,7 +40,7 @@ fn disable_entities_on_click( valid_query: Query<&DisableOnClick>, mut commands: Commands, ) { - let clicked_entity = trigger.target().unwrap(); + let clicked_entity = trigger.target(); // Windows and text are entities and can be clicked! // We definitely don't want to disable the window itself, // because that would cause the app to close! diff --git a/examples/ecs/event.rs b/examples/ecs/event.rs index e01d810099..33ff7ad60a 100644 --- a/examples/ecs/event.rs +++ b/examples/ecs/event.rs @@ -6,17 +6,17 @@ use bevy::prelude::*; // In order to send or receive events first you must define them // This event should be sent when something attempts to deal damage to another entity. -#[derive(Event, Debug)] +#[derive(Event, BufferedEvent, Debug)] struct DealDamage { pub amount: i32, } // This event should be sent when an entity receives damage. -#[derive(Event, Debug, Default)] +#[derive(Event, BufferedEvent, Debug, Default)] struct DamageReceived; // This event should be sent when an entity blocks damage with armor. -#[derive(Event, Debug, Default)] +#[derive(Event, BufferedEvent, Debug, Default)] struct ArmorBlockedDamage; // This resource represents a timer used to determine when to deal damage diff --git a/examples/ecs/observer_propagation.rs b/examples/ecs/observer_propagation.rs index cea184b3aa..b2da6c9d97 100644 --- a/examples/ecs/observer_propagation.rs +++ b/examples/ecs/observer_propagation.rs @@ -53,8 +53,8 @@ fn setup(mut commands: Commands) { // - **auto_propagate:** // We can also choose whether or not this event will propagate by default when triggered. If this is // false, it will only propagate following a call to `On::propagate(true)`. -#[derive(Clone, Component, Event)] -#[event(traversal = &'static ChildOf, auto_propagate)] +#[derive(Clone, Component, Event, EntityEvent)] +#[entity_event(traversal = &'static ChildOf, auto_propagate)] struct Attack { damage: u16, } @@ -78,14 +78,14 @@ fn attack_armor(entities: Query>, mut commands: Commands) { } fn attack_hits(trigger: On, name: Query<&Name>) { - if let Ok(name) = name.get(trigger.target().unwrap()) { + if let Ok(name) = name.get(trigger.target()) { info!("Attack hit {}", name); } } /// A callback placed on [`Armor`], checking if it absorbed all the [`Attack`] damage. fn block_attack(mut trigger: On, armor: Query<(&Armor, &Name)>) { - let (armor, name) = armor.get(trigger.target().unwrap()).unwrap(); + let (armor, name) = armor.get(trigger.target()).unwrap(); let attack = trigger.event_mut(); let damage = attack.damage.saturating_sub(**armor); if damage > 0 { @@ -110,14 +110,14 @@ fn take_damage( mut app_exit: EventWriter, ) { let attack = trigger.event(); - let (mut hp, name) = hp.get_mut(trigger.target().unwrap()).unwrap(); + let (mut hp, name) = hp.get_mut(trigger.target()).unwrap(); **hp = hp.saturating_sub(attack.damage); if **hp > 0 { info!("{} has {:.1} HP", name, hp.0); } else { warn!("💀 {} has died a gruesome death", name); - commands.entity(trigger.target().unwrap()).despawn(); + commands.entity(trigger.target()).despawn(); app_exit.write(AppExit::Success); } diff --git a/examples/ecs/observers.rs b/examples/ecs/observers.rs index e27a1ad7e2..bf40f08ec3 100644 --- a/examples/ecs/observers.rs +++ b/examples/ecs/observers.rs @@ -60,13 +60,13 @@ impl Mine { } } -#[derive(Event)] +#[derive(Event, EntityEvent)] struct ExplodeMines { pos: Vec2, radius: f32, } -#[derive(Event)] +#[derive(Event, EntityEvent)] struct Explode; fn setup(mut commands: Commands) { @@ -113,33 +113,29 @@ fn setup(mut commands: Commands) { } fn on_add_mine(trigger: On, query: Query<&Mine>, mut index: ResMut) { - let mine = query.get(trigger.target().unwrap()).unwrap(); + let mine = query.get(trigger.target()).unwrap(); let tile = ( (mine.pos.x / CELL_SIZE).floor() as i32, (mine.pos.y / CELL_SIZE).floor() as i32, ); - index - .map - .entry(tile) - .or_default() - .insert(trigger.target().unwrap()); + index.map.entry(tile).or_default().insert(trigger.target()); } // Remove despawned mines from our index fn on_remove_mine(trigger: On, query: Query<&Mine>, mut index: ResMut) { - let mine = query.get(trigger.target().unwrap()).unwrap(); + let mine = query.get(trigger.target()).unwrap(); let tile = ( (mine.pos.x / CELL_SIZE).floor() as i32, (mine.pos.y / CELL_SIZE).floor() as i32, ); index.map.entry(tile).and_modify(|set| { - set.remove(&trigger.target().unwrap()); + set.remove(&trigger.target()); }); } fn explode_mine(trigger: On, query: Query<&Mine>, mut commands: Commands) { // If a triggered event is targeting a specific entity you can access it with `.target()` - let id = trigger.target().unwrap(); + let id = trigger.target(); let Ok(mut entity) = commands.get_entity(id) else { return; }; diff --git a/examples/ecs/removal_detection.rs b/examples/ecs/removal_detection.rs index c247d1bf9b..699bb66e07 100644 --- a/examples/ecs/removal_detection.rs +++ b/examples/ecs/removal_detection.rs @@ -50,7 +50,7 @@ fn remove_component( fn react_on_removal(trigger: On, mut query: Query<&mut Sprite>) { // The `Remove` trigger was automatically called on the `Entity` that had its `MyComponent` removed. - let entity = trigger.target().unwrap(); + let entity = trigger.target(); if let Ok(mut sprite) = query.get_mut(entity) { sprite.color = Color::srgb(0.5, 1., 1.); } diff --git a/examples/ecs/send_and_receive_events.rs b/examples/ecs/send_and_receive_events.rs index c9cb334503..78dd5b240b 100644 --- a/examples/ecs/send_and_receive_events.rs +++ b/examples/ecs/send_and_receive_events.rs @@ -1,7 +1,7 @@ //! From time to time, you may find that you want to both send and receive an event of the same type in a single system. //! //! Of course, this results in an error: the borrows of [`EventWriter`] and [`EventReader`] overlap, -//! if and only if the [`Event`] type is the same. +//! if and only if the [`BufferedEvent`] type is the same. //! One system parameter borrows the [`Events`] resource mutably, and another system parameter borrows the [`Events`] resource immutably. //! If Bevy allowed this, this would violate Rust's rules against aliased mutability. //! In other words, this would be Undefined Behavior (UB)! @@ -46,10 +46,10 @@ fn main() { app.update(); } -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct A; -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct B; // This works fine, because the types are different, @@ -62,7 +62,7 @@ fn read_and_write_different_event_types(mut a: EventWriter, mut b: EventReade } /// A dummy event type. -#[derive(Debug, Clone, Event)] +#[derive(Debug, Clone, Event, BufferedEvent)] struct DebugEvent { resend_from_param_set: bool, resend_from_local_event_reader: bool, diff --git a/examples/games/breakout.rs b/examples/games/breakout.rs index 0a0b04f7a2..b8d02e8159 100644 --- a/examples/games/breakout.rs +++ b/examples/games/breakout.rs @@ -89,7 +89,7 @@ struct Ball; #[derive(Component, Deref, DerefMut)] struct Velocity(Vec2); -#[derive(Event, Default)] +#[derive(Event, BufferedEvent, Default)] struct CollisionEvent; #[derive(Component)] diff --git a/examples/helpers/widgets.rs b/examples/helpers/widgets.rs index 14873acc5f..02039fed49 100644 --- a/examples/helpers/widgets.rs +++ b/examples/helpers/widgets.rs @@ -6,7 +6,7 @@ use bevy::{ecs::system::EntityCommands, prelude::*}; /// An event that's sent whenever the user changes one of the settings by /// clicking a radio button. -#[derive(Clone, Event, Deref, DerefMut)] +#[derive(Clone, Event, BufferedEvent, Deref, DerefMut)] pub struct WidgetClickEvent(T); /// A marker component that we place on all widgets that send diff --git a/examples/no_std/library/src/lib.rs b/examples/no_std/library/src/lib.rs index e336d8a98e..a193ed0240 100644 --- a/examples/no_std/library/src/lib.rs +++ b/examples/no_std/library/src/lib.rs @@ -106,7 +106,7 @@ struct DelayedComponentTimer(Timer); #[component(immutable)] struct DelayedComponent(B); -#[derive(Event)] +#[derive(Event, EntityEvent)] struct Unwrap; fn tick_timers( @@ -127,10 +127,7 @@ fn tick_timers( } fn unwrap(trigger: On, world: &mut World) { - if let Some(mut target) = trigger - .target() - .and_then(|target| world.get_entity_mut(target).ok()) - { + if let Ok(mut target) = world.get_entity_mut(trigger.target()) { if let Some(DelayedComponent(bundle)) = target.take::>() { target.insert(bundle); } diff --git a/examples/picking/debug_picking.rs b/examples/picking/debug_picking.rs index 7f8c26a4bf..de6fbf0bca 100644 --- a/examples/picking/debug_picking.rs +++ b/examples/picking/debug_picking.rs @@ -48,13 +48,13 @@ fn setup_scene( .observe(on_click_spawn_cube) .observe( |out: On>, mut texts: Query<&mut TextColor>| { - let mut text_color = texts.get_mut(out.target().unwrap()).unwrap(); + let mut text_color = texts.get_mut(out.target()).unwrap(); text_color.0 = Color::WHITE; }, ) .observe( |over: On>, mut texts: Query<&mut TextColor>| { - let mut color = texts.get_mut(over.target().unwrap()).unwrap(); + let mut color = texts.get_mut(over.target()).unwrap(); color.0 = bevy::color::palettes::tailwind::CYAN_400.into(); }, ); @@ -102,7 +102,7 @@ fn on_click_spawn_cube( } fn on_drag_rotate(drag: On>, mut transforms: Query<&mut Transform>) { - if let Ok(mut transform) = transforms.get_mut(drag.target().unwrap()) { + if let Ok(mut transform) = transforms.get_mut(drag.target()) { transform.rotate_y(drag.delta.x * 0.02); transform.rotate_x(drag.delta.y * 0.02); } diff --git a/examples/picking/mesh_picking.rs b/examples/picking/mesh_picking.rs index 60d0cb64ab..d1fa90c93f 100644 --- a/examples/picking/mesh_picking.rs +++ b/examples/picking/mesh_picking.rs @@ -157,14 +157,14 @@ fn setup_scene( } /// Returns an observer that updates the entity's material to the one specified. -fn update_material_on( +fn update_material_on( new_material: Handle, ) -> impl Fn(On, Query<&mut MeshMaterial3d>) { // An observer closure that captures `new_material`. We do this to avoid needing to write four // versions of this observer, each triggered by a different event and with a different hardcoded // material. Instead, the event type is a generic, and the material is passed in. move |trigger, mut query| { - if let Ok(mut material) = query.get_mut(trigger.target().unwrap()) { + if let Ok(mut material) = query.get_mut(trigger.target()) { material.0 = new_material.clone(); } } @@ -191,7 +191,7 @@ fn rotate(mut query: Query<&mut Transform, With>, time: Res { - type Fetch<'w> = AssetChangedFetch<'w, A>; + type Fetch<'w, 's> = AssetChangedFetch<'w, A>; type State = AssetChangedState; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { 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> { + ) -> Self::Fetch<'w, 's> { // SAFETY: // - `AssetChanges` is private and only accessed mutably in the `AssetEventSystems` system set. // - `resource_id` was obtained from the type ID of `AssetChanges`. @@ -201,9 +203,9 @@ unsafe impl WorldQuery for AssetChanged { const IS_DENSE: bool = <&A>::IS_DENSE; - unsafe fn set_archetype<'w>( - fetch: &mut Self::Fetch<'w>, - state: &Self::State, + unsafe fn set_archetype<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table, ) { @@ -215,7 +217,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, 's>, + 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,7 +271,7 @@ unsafe impl QueryFilter for AssetChanged { #[inline] unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_>, + fetch: &mut Self::Fetch<'_, '_>, entity: Entity, table_row: TableRow, ) -> bool { 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/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/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..81744f74a5 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()); diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index 4e4529e631..44021f27a7 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -76,6 +76,7 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { let user_generics_with_world = { let mut generics = ast.generics; generics.params.insert(0, parse_quote!('__w)); + generics.params.insert(0, parse_quote!('__s)); generics }; let (user_impl_generics_with_world, user_ty_generics_with_world, user_where_clauses_with_world) = @@ -256,11 +257,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; - 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,16 +279,28 @@ 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>( - _fetch: &mut ::Fetch<'__w>, + unsafe fn fetch<'__w, '__s>( + _fetch: &mut ::Fetch<'__w, '__s>, _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),)* } } } + + 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),)* + } + } + } } } else { quote! {} @@ -301,11 +314,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; - 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,17 +336,29 @@ 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>( - _fetch: &mut ::Fetch<'__w>, + unsafe fn fetch<'__w, '__s>( + _fetch: &mut ::Fetch<'__w, '__s>, _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),)* } } } + 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),)* + } + } + } + #read_only_data_impl } }; diff --git a/crates/bevy_ecs/macros/src/query_filter.rs b/crates/bevy_ecs/macros/src/query_filter.rs index c7ddb9cc83..acc5c3d41b 100644 --- a/crates/bevy_ecs/macros/src/query_filter.rs +++ b/crates/bevy_ecs/macros/src/query_filter.rs @@ -23,6 +23,7 @@ pub fn derive_query_filter_impl(input: TokenStream) -> TokenStream { let user_generics_with_world = { let mut generics = ast.generics; generics.params.insert(0, parse_quote!('__w)); + generics.params.insert(0, parse_quote!('__s)); generics }; let (user_impl_generics_with_world, user_ty_generics_with_world, user_where_clauses_with_world) = @@ -101,8 +102,8 @@ pub fn derive_query_filter_impl(input: TokenStream) -> TokenStream { #[allow(unused_variables)] #[inline(always)] - unsafe fn filter_fetch<'__w>( - _fetch: &mut ::Fetch<'__w>, + unsafe fn filter_fetch<'__w, '__s>( + _fetch: &mut ::Fetch<'__w, '__s>, _entity: #path::entity::Entity, _table_row: #path::storage::TableRow, ) -> bool { diff --git a/crates/bevy_ecs/macros/src/world_query.rs b/crates/bevy_ecs/macros/src/world_query.rs index 5c4c0bff01..c064b05b97 100644 --- a/crates/bevy_ecs/macros/src/world_query.rs +++ b/crates/bevy_ecs/macros/src/world_query.rs @@ -34,14 +34,14 @@ pub(crate) fn item_struct( #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>,)* + #(#(#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>, )* + #( #field_visibilities <#field_types as #path::query::QueryData>::Item<'__w, '__s>, )* ); }, Fields::Unit => quote! { @@ -78,8 +78,8 @@ 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 (), + #(#named_field_idents: <#field_types as #path::query::WorldQuery>::Fetch<'__w, '__s>,)* + #marker_name: (&'__w(), &'__s()), } impl #user_impl_generics_with_world Clone for #fetch_struct_name #user_ty_generics_with_world @@ -87,7 +87,7 @@ pub(crate) fn world_query_impl( fn clone(&self) -> Self { Self { #(#named_field_idents: self.#named_field_idents.clone(),)* - #marker_name: &(), + #marker_name: (&(), &()), } } } @@ -96,26 +96,26 @@ pub(crate) fn world_query_impl( unsafe impl #user_impl_generics #path::query::WorldQuery for #struct_name #user_ty_generics #user_where_clauses { - type Fetch<'__w> = #fetch_struct_name #user_ty_generics_with_world; + type Fetch<'__w, '__s> = #fetch_struct_name #user_ty_generics_with_world; type State = #state_struct_name #user_ty_generics; - fn shrink_fetch<'__wlong: '__wshort, '__wshort>( - fetch: <#struct_name #user_ty_generics as #path::query::WorldQuery>::Fetch<'__wlong> - ) -> <#struct_name #user_ty_generics as #path::query::WorldQuery>::Fetch<'__wshort> { + fn shrink_fetch<'__wlong: '__wshort, '__wshort, '__s>( + fetch: <#struct_name #user_ty_generics as #path::query::WorldQuery>::Fetch<'__wlong, '__s> + ) -> <#struct_name #user_ty_generics as #path::query::WorldQuery>::Fetch<'__wshort, '__s> { #fetch_struct_name { #( #named_field_idents: <#field_types>::shrink_fetch(fetch.#named_field_idents), )* - #marker_name: &(), + #marker_name: (&(), &()), } } - 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> { + ) -> ::Fetch<'__w, '__s> { #fetch_struct_name { #(#named_field_idents: <#field_types>::init_fetch( @@ -125,7 +125,7 @@ pub(crate) fn world_query_impl( _this_run, ), )* - #marker_name: &(), + #marker_name: (&(), &()), } } @@ -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>( - _fetch: &mut ::Fetch<'__w>, - _state: &Self::State, + unsafe fn set_archetype<'__w, '__s>( + _fetch: &mut ::Fetch<'__w, '__s>, + _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>( - _fetch: &mut ::Fetch<'__w>, - _state: &Self::State, + unsafe fn set_table<'__w, '__s>( + _fetch: &mut ::Fetch<'__w, '__s>, + _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/name.rs b/crates/bevy_ecs/src/name.rs index cd2e946678..67719ca18d 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -141,7 +141,7 @@ pub struct NameOrEntity { pub entity: Entity, } -impl<'a> core::fmt::Display for NameOrEntityItem<'a> { +impl<'w, 's> core::fmt::Display for NameOrEntityItem<'w, 's> { #[inline(always)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { match self.name { @@ -274,9 +274,9 @@ mod tests { let e2 = world.spawn(name.clone()).id(); let mut query = world.query::(); let d1 = query.get(&world, e1).unwrap(); - let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities without a Name should be {index}v{generation} assert_eq!(d1.to_string(), "0v0"); + let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities with a Name should be the Name assert_eq!(d2.to_string(), "MyName"); } diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index ef8b0b6042..4db65b888d 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -1078,7 +1078,7 @@ mod tests { struct ChildOf(Entity); impl Traversal for &'_ ChildOf { - fn traverse(item: Self::Item<'_>, _: &D) -> Option { + fn traverse(item: Self::Item<'_, '_>, _: &D) -> Option { Some(item.0) } } diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 55fa42c41e..ba1a85cec3 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -163,7 +163,7 @@ use variadics_please::all_tuples; /// } /// /// // `HealthQueryItem` is only available when accessing the query with mutable methods. -/// impl<'w> HealthQueryItem<'w> { +/// impl<'w, 's> HealthQueryItem<'w, 's> { /// fn damage(&mut self, value: f32) { /// self.health.0 -= value; /// } @@ -174,7 +174,7 @@ use variadics_please::all_tuples; /// } /// /// // `HealthQueryReadOnlyItem` is only available when accessing the query with immutable methods. -/// impl<'w> HealthQueryReadOnlyItem<'w> { +/// impl<'w, 's> HealthQueryReadOnlyItem<'w, 's> { /// fn total(&self) -> f32 { /// self.health.0 + self.buff.map_or(0.0, |Buff(buff)| *buff) /// } @@ -290,10 +290,12 @@ pub unsafe trait QueryData: WorldQuery { /// The item returned by this [`WorldQuery`] /// This will be the data retrieved by the query, /// and is visible to the end user when calling e.g. `Query::get`. - type Item<'a>; + type Item<'w, 's>; /// This function manually implements subtyping for the query items. - 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>; /// Offers additional access above what we requested in `update_component_access`. /// Implementations may add additional access that is a subset of `available_access` @@ -322,11 +324,11 @@ pub unsafe trait QueryData: WorldQuery { /// - Must always be called _after_ [`WorldQuery::set_table`] or [`WorldQuery::set_archetype`]. `entity` and /// `table_row` must be in the range of the current table and archetype. /// - There must not be simultaneous conflicting component access registered in `update_component_access`. - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w>; + ) -> Self::Item<'w, 's>; } /// A [`QueryData`] that is read only. @@ -337,40 +339,58 @@ pub unsafe trait QueryData: WorldQuery { pub unsafe trait ReadOnlyQueryData: QueryData {} /// The item type returned when a [`WorldQuery`] is iterated over -pub type QueryItem<'w, Q> = ::Item<'w>; +pub type QueryItem<'w, 's, Q> = ::Item<'w, 's>; /// The read-only variant of the item type returned when a [`QueryData`] is iterated over immutably -pub type ROQueryItem<'w, D> = QueryItem<'w, ::ReadOnly>; +pub type ROQueryItem<'w, 's, D> = QueryItem<'w, 's, ::ReadOnly>; + +/// A [`QueryData`] that does not borrow from its [`QueryState`](crate::query::QueryState). +/// +/// This is implemented by most `QueryData` types. +/// The main exceptions are [`FilteredEntityRef`], [`FilteredEntityMut`], [`EntityRefExcept`], and [`EntityMutExcept`], +/// which borrow an access list from their query state. +/// Consider using a full [`EntityRef`] or [`EntityMut`] if you would need those. +pub trait ReleaseStateQueryData: QueryData { + /// Releases the borrow from the query state by converting an item to have a `'static` state lifetime. + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static>; +} /// SAFETY: /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for Entity { - type Fetch<'w> = (); + type Fetch<'w, 's> = (); type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + _: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { + } - 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> { + ) -> Self::Fetch<'w, 's> { } const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - 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, 's>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -394,18 +414,20 @@ unsafe impl QueryData for Entity { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = Entity; + type Item<'w, 's> = Entity; - 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 } #[inline(always)] - unsafe fn fetch<'w>( - _fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { entity } } @@ -413,23 +435,31 @@ unsafe impl QueryData for Entity { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for Entity {} +impl ReleaseStateQueryData for Entity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// SAFETY: /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for EntityLocation { - type Fetch<'w> = &'w Entities; + type Fetch<'w, 's> = &'w Entities; type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { 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> { + ) -> Self::Fetch<'w, 's> { world.entities() } @@ -438,16 +468,20 @@ unsafe impl WorldQuery for EntityLocation { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - 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, 's>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -470,18 +504,20 @@ unsafe impl WorldQuery for EntityLocation { unsafe impl QueryData for EntityLocation { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = EntityLocation; + type Item<'w, 's> = EntityLocation; - 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 } #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world unsafe { fetch.get(entity).debug_checked_unwrap() } } @@ -490,6 +526,12 @@ unsafe impl QueryData for EntityLocation { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for EntityLocation {} +impl ReleaseStateQueryData for EntityLocation { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The `SpawnDetails` query parameter fetches the [`Tick`] the entity was spawned at. /// /// To evaluate whether the spawn happened since the last time the system ran, the system @@ -563,19 +605,21 @@ pub struct SpawnDetailsFetch<'w> { // SAFETY: // No components are accessed. unsafe impl WorldQuery for SpawnDetails { - type Fetch<'w> = SpawnDetailsFetch<'w>; + type Fetch<'w, 's> = SpawnDetailsFetch<'w>; type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { 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> { + ) -> Self::Fetch<'w, 's> { SpawnDetailsFetch { entities: world.entities(), last_run, @@ -586,16 +630,20 @@ unsafe impl WorldQuery for SpawnDetails { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table, ) { } #[inline] - 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, 's>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -620,18 +668,20 @@ unsafe impl WorldQuery for SpawnDetails { unsafe impl QueryData for SpawnDetails { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = Self; + type Item<'w, 's> = Self; - 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 } #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: only living entities are queried let (spawned_by, spawned_at) = unsafe { fetch @@ -650,6 +700,12 @@ unsafe impl QueryData for SpawnDetails { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for SpawnDetails {} +impl ReleaseStateQueryData for SpawnDetails { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The [`WorldQuery::Fetch`] type for WorldQueries that can fetch multiple components from an entity /// ([`EntityRef`], [`EntityMut`], etc.) #[derive(Copy, Clone)] @@ -665,19 +721,21 @@ pub struct EntityFetch<'w> { /// This is sound because `update_component_access` sets read access for all components and panic when appropriate. /// Filters are unchanged. unsafe impl<'a> WorldQuery for EntityRef<'a> { - type Fetch<'w> = EntityFetch<'w>; + type Fetch<'w, 's> = EntityFetch<'w>; type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { 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> { + ) -> Self::Fetch<'w, 's> { EntityFetch { world, last_run, @@ -688,16 +746,20 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - 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, 's>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) { @@ -726,18 +788,20 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { unsafe impl<'a> QueryData for EntityRef<'a> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = EntityRef<'w>; + type Item<'w, 's> = EntityRef<'w>; - 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 } #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -753,21 +817,29 @@ unsafe impl<'a> QueryData for EntityRef<'a> { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for EntityRef<'_> {} +impl ReleaseStateQueryData for EntityRef<'_> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for EntityMut<'a> { - type Fetch<'w> = EntityFetch<'w>; + type Fetch<'w, 's> = EntityFetch<'w>; type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { 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> { + ) -> Self::Fetch<'w, 's> { EntityFetch { world, last_run, @@ -778,16 +850,20 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - 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, 's>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) { @@ -816,18 +892,20 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { unsafe impl<'a> QueryData for EntityMut<'a> { const IS_READ_ONLY: bool = false; type ReadOnly = EntityRef<'a>; - type Item<'w> = EntityMut<'w>; + type Item<'w, 's> = EntityMut<'w>; - 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 } #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -840,23 +918,31 @@ unsafe impl<'a> QueryData for EntityMut<'a> { } } +impl ReleaseStateQueryData for EntityMut<'_> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { - type Fetch<'w> = (EntityFetch<'w>, Access); + type Fetch<'w, 's> = (EntityFetch<'w>, Access); type State = Access; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } const IS_DENSE: bool = false; - 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> { + ) -> Self::Fetch<'w, 's> { let mut access = Access::default(); access.read_all_components(); ( @@ -870,9 +956,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { } #[inline] - unsafe fn set_archetype<'w>( - fetch: &mut Self::Fetch<'w>, - state: &Self::State, + unsafe fn set_archetype<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, _: &'w Archetype, _table: &Table, ) { @@ -880,7 +966,11 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, _: &'w Table) { + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, + _: &'w Table, + ) { fetch.1.clone_from(state); } @@ -915,9 +1005,11 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { unsafe impl<'a> QueryData for FilteredEntityRef<'a> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = FilteredEntityRef<'w>; + type Item<'w, 's> = FilteredEntityRef<'w>; - 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 } @@ -941,11 +1033,11 @@ unsafe impl<'a> QueryData for FilteredEntityRef<'a> { } #[inline(always)] - unsafe fn fetch<'w>( - (fetch, access): &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + (fetch, access): &mut Self::Fetch<'w, 's>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -963,21 +1055,23 @@ unsafe impl ReadOnlyQueryData for FilteredEntityRef<'_> {} /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { - type Fetch<'w> = (EntityFetch<'w>, Access); + type Fetch<'w, 's> = (EntityFetch<'w>, Access); type State = Access; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } const IS_DENSE: bool = false; - 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> { + ) -> Self::Fetch<'w, 's> { let mut access = Access::default(); access.write_all_components(); ( @@ -991,9 +1085,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { } #[inline] - unsafe fn set_archetype<'w>( - fetch: &mut Self::Fetch<'w>, - state: &Self::State, + unsafe fn set_archetype<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, _: &'w Archetype, _table: &Table, ) { @@ -1001,7 +1095,11 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, _: &'w Table) { + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, + _: &'w Table, + ) { fetch.1.clone_from(state); } @@ -1036,9 +1134,11 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { unsafe impl<'a> QueryData for FilteredEntityMut<'a> { const IS_READ_ONLY: bool = false; type ReadOnly = FilteredEntityRef<'a>; - type Item<'w> = FilteredEntityMut<'w>; + type Item<'w, 's> = FilteredEntityMut<'w>; - 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 } @@ -1060,11 +1160,11 @@ unsafe impl<'a> QueryData for FilteredEntityMut<'a> { } #[inline(always)] - unsafe fn fetch<'w>( - (fetch, access): &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + (fetch, access): &mut Self::Fetch<'w, 's>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -1084,19 +1184,21 @@ unsafe impl<'a, B> WorldQuery for EntityRefExcept<'a, B> where B: Bundle, { - type Fetch<'w> = EntityFetch<'w>; + type Fetch<'w, 's> = EntityFetch<'w>; type State = SmallVec<[ComponentId; 4]>; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _: &Self::State, + _: &'s Self::State, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w> { + ) -> Self::Fetch<'w, 's> { EntityFetch { world, last_run, @@ -1106,15 +1208,15 @@ where const IS_DENSE: bool = true; - unsafe fn set_archetype<'w>( - _: &mut Self::Fetch<'w>, - _: &Self::State, + unsafe fn set_archetype<'w, 's>( + _: &mut Self::Fetch<'w, 's>, + _: &'s Self::State, _: &'w Archetype, _: &'w Table, ) { } - unsafe fn set_table<'w>(_: &mut Self::Fetch<'w>, _: &Self::State, _: &'w Table) {} + unsafe fn set_table<'w, 's>(_: &mut Self::Fetch<'w, 's>, _: &'s Self::State, _: &'w Table) {} fn update_component_access( state: &Self::State, @@ -1161,17 +1263,19 @@ where { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = EntityRefExcept<'w, B>; + type Item<'w, 's> = EntityRefExcept<'w, B>; - 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 } - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, _: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let cell = fetch .world .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) @@ -1191,19 +1295,21 @@ unsafe impl<'a, B> WorldQuery for EntityMutExcept<'a, B> where B: Bundle, { - type Fetch<'w> = EntityFetch<'w>; + type Fetch<'w, 's> = EntityFetch<'w>; type State = SmallVec<[ComponentId; 4]>; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _: &Self::State, + _: &'s Self::State, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w> { + ) -> Self::Fetch<'w, 's> { EntityFetch { world, last_run, @@ -1213,15 +1319,15 @@ where const IS_DENSE: bool = true; - unsafe fn set_archetype<'w>( - _: &mut Self::Fetch<'w>, - _: &Self::State, + unsafe fn set_archetype<'w, 's>( + _: &mut Self::Fetch<'w, 's>, + _: &'s Self::State, _: &'w Archetype, _: &'w Table, ) { } - unsafe fn set_table<'w>(_: &mut Self::Fetch<'w>, _: &Self::State, _: &'w Table) {} + unsafe fn set_table<'w, 's>(_: &mut Self::Fetch<'w, 's>, _: &'s Self::State, _: &'w Table) {} fn update_component_access( state: &Self::State, @@ -1269,17 +1375,19 @@ where { const IS_READ_ONLY: bool = false; type ReadOnly = EntityRefExcept<'a, B>; - type Item<'w> = EntityMutExcept<'w, B>; + type Item<'w, 's> = EntityMutExcept<'w, B>; - 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 } - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, _: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let cell = fetch .world .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) @@ -1292,19 +1400,21 @@ where /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for &Archetype { - type Fetch<'w> = (&'w Entities, &'w Archetypes); + type Fetch<'w, 's> = (&'w Entities, &'w Archetypes); type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { 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> { + ) -> Self::Fetch<'w, 's> { (world.entities(), world.archetypes()) } @@ -1313,16 +1423,20 @@ unsafe impl WorldQuery for &Archetype { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - 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, 's>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -1345,18 +1459,20 @@ unsafe impl WorldQuery for &Archetype { unsafe impl QueryData for &Archetype { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = &'w Archetype; + type Item<'w, 's> = &'w Archetype; - 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 } #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let (entities, archetypes) = *fetch; // SAFETY: `fetch` must be called with an entity that exists in the world let location = unsafe { entities.get(entity).debug_checked_unwrap() }; @@ -1368,6 +1484,12 @@ unsafe impl QueryData for &Archetype { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for &Archetype {} +impl ReleaseStateQueryData for &Archetype { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The [`WorldQuery::Fetch`] type for `& T`. pub struct ReadFetch<'w, T: Component> { components: StorageSwitch< @@ -1392,15 +1514,17 @@ impl Copy for ReadFetch<'_, T> {} /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl WorldQuery for &T { - type Fetch<'w> = ReadFetch<'w, T>; + type Fetch<'w, 's> = ReadFetch<'w, T>; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, &component_id: &ComponentId, _last_run: Tick, @@ -1490,18 +1614,20 @@ unsafe impl WorldQuery for &T { unsafe impl QueryData for &T { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = &'w T; + type Item<'w, 's> = &'w T; - fn shrink<'wlong: 'wshort, 'wshort>(item: &'wlong T) -> &'wshort T { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch.components.extract( |table| { // SAFETY: set_table was previously called @@ -1527,6 +1653,12 @@ unsafe impl QueryData for &T { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for &T {} +impl ReleaseStateQueryData for &T { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + #[doc(hidden)] pub struct RefFetch<'w, T: Component> { components: StorageSwitch< @@ -1559,15 +1691,17 @@ impl Copy for RefFetch<'_, T> {} /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { - type Fetch<'w> = RefFetch<'w, T>; + type Fetch<'w, 's> = RefFetch<'w, T>; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, &component_id: &ComponentId, last_run: Tick, @@ -1666,18 +1800,20 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = Ref<'w, T>; + type Item<'w, 's> = Ref<'w, T>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Ref<'wlong, T>) -> Ref<'wshort, T> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch.components.extract( |table| { // SAFETY: set_table was previously called @@ -1726,6 +1862,12 @@ unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> { /// SAFETY: access is read only unsafe impl<'__w, T: Component> ReadOnlyQueryData for Ref<'__w, T> {} +impl ReleaseStateQueryData for Ref<'_, T> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The [`WorldQuery::Fetch`] type for `&mut T`. pub struct WriteFetch<'w, T: Component> { components: StorageSwitch< @@ -1758,15 +1900,17 @@ impl Copy for WriteFetch<'_, T> {} /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { - type Fetch<'w> = WriteFetch<'w, T>; + type Fetch<'w, 's> = WriteFetch<'w, T>; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, &component_id: &ComponentId, last_run: Tick, @@ -1865,18 +2009,20 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { unsafe impl<'__w, T: Component> QueryData for &'__w mut T { const IS_READ_ONLY: bool = false; type ReadOnly = &'__w T; - type Item<'w> = Mut<'w, T>; + type Item<'w, 's> = Mut<'w, T>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Mut<'wlong, T>) -> Mut<'wshort, T> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch.components.extract( |table| { // SAFETY: set_table was previously called @@ -1922,6 +2068,12 @@ unsafe impl<'__w, T: Component> QueryData for &'__w mut T } } +impl> ReleaseStateQueryData for &mut T { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// When `Mut` is used in a query, it will be converted to `Ref` when transformed into its read-only form, providing access to change detection methods. /// /// By contrast `&mut T` will result in a `Mut` item in mutable form to record mutations, but result in a bare `&T` in read-only form. @@ -1932,16 +2084,18 @@ unsafe impl<'__w, T: Component> QueryData for &'__w mut T /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { - type Fetch<'w> = WriteFetch<'w, T>; + type Fetch<'w, 's> = WriteFetch<'w, T>; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } #[inline] // Forwarded to `&mut T` - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, state: &ComponentId, last_run: Tick, @@ -2008,33 +2162,41 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { unsafe impl<'__w, T: Component> QueryData for Mut<'__w, T> { const IS_READ_ONLY: bool = false; type ReadOnly = Ref<'__w, T>; - type Item<'w> = Mut<'w, T>; + type Item<'w, 's> = Mut<'w, T>; // Forwarded to `&mut T` - fn shrink<'wlong: 'wshort, 'wshort>(item: Mut<'wlong, T>) -> Mut<'wshort, T> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { <&mut T as QueryData>::shrink(item) } #[inline(always)] // Forwarded to `&mut T` - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( // Rust complains about lifetime bounds not matching the trait if I directly use `WriteFetch<'w, T>` right here. // But it complains nowhere else in the entire trait implementation. - fetch: &mut Self::Fetch<'w>, + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, table_row: TableRow, - ) -> Mut<'w, T> { + ) -> Self::Item<'w, 's> { <&mut T as QueryData>::fetch(fetch, entity, table_row) } } +impl> ReleaseStateQueryData for Mut<'_, T> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + #[doc(hidden)] -pub struct OptionFetch<'w, T: WorldQuery> { - fetch: T::Fetch<'w>, +pub struct OptionFetch<'w, 's, T: WorldQuery> { + fetch: T::Fetch<'w, 's>, matches: bool, } -impl Clone for OptionFetch<'_, T> { +impl Clone for OptionFetch<'_, '_, T> { fn clone(&self) -> Self { Self { fetch: self.fetch.clone(), @@ -2048,10 +2210,12 @@ impl Clone for OptionFetch<'_, T> { /// This is sound because `update_component_access` adds the same accesses as `T`. /// Filters are unchanged. unsafe impl WorldQuery for Option { - type Fetch<'w> = OptionFetch<'w, T>; + type Fetch<'w, 's> = OptionFetch<'w, 's, T>; type State = T::State; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { OptionFetch { fetch: T::shrink_fetch(fetch.fetch), matches: fetch.matches, @@ -2059,12 +2223,12 @@ unsafe impl WorldQuery for Option { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - state: &T::State, + state: &'s T::State, last_run: Tick, this_run: Tick, - ) -> OptionFetch<'w, T> { + ) -> OptionFetch<'w, 's, T> { OptionFetch { // SAFETY: The invariants are upheld by the caller. fetch: unsafe { T::init_fetch(world, state, last_run, this_run) }, @@ -2075,9 +2239,9 @@ unsafe impl WorldQuery for Option { const IS_DENSE: bool = T::IS_DENSE; #[inline] - unsafe fn set_archetype<'w>( - fetch: &mut OptionFetch<'w, T>, - state: &T::State, + unsafe fn set_archetype<'w, 's>( + fetch: &mut OptionFetch<'w, 's, T>, + state: &'s T::State, archetype: &'w Archetype, table: &'w Table, ) { @@ -2091,7 +2255,11 @@ unsafe impl WorldQuery for Option { } #[inline] - unsafe fn set_table<'w>(fetch: &mut OptionFetch<'w, T>, state: &T::State, table: &'w Table) { + unsafe fn set_table<'w, 's>( + fetch: &mut OptionFetch<'w, 's, T>, + state: &'s T::State, + table: &'w Table, + ) { fetch.matches = T::matches_component_set(state, &|id| table.has_column(id)); if fetch.matches { // SAFETY: The invariants are upheld by the caller. @@ -2136,18 +2304,20 @@ unsafe impl WorldQuery for Option { unsafe impl QueryData for Option { const IS_READ_ONLY: bool = T::IS_READ_ONLY; type ReadOnly = Option; - type Item<'w> = Option>; + type Item<'w, 's> = Option>; - 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.map(T::shrink) } #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch .matches // SAFETY: The invariants are upheld by the caller. @@ -2158,6 +2328,12 @@ unsafe impl QueryData for Option { /// SAFETY: [`OptionFetch`] is read only because `T` is read only unsafe impl ReadOnlyQueryData for Option {} +impl ReleaseStateQueryData for Option { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item.map(T::release_state) + } +} + /// Returns a bool that describes if an entity has the component `T`. /// /// This can be used in a [`Query`](crate::system::Query) if you want to know whether or not entities @@ -2233,20 +2409,22 @@ impl core::fmt::Debug for Has { /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for Has { - type Fetch<'w> = bool; + type Fetch<'w, 's> = bool; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } #[inline] - 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> { + ) -> Self::Fetch<'w, 's> { false } @@ -2258,9 +2436,9 @@ unsafe impl WorldQuery for Has { }; #[inline] - unsafe fn set_archetype<'w>( - fetch: &mut Self::Fetch<'w>, - state: &Self::State, + unsafe fn set_archetype<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, archetype: &'w Archetype, _table: &Table, ) { @@ -2268,7 +2446,11 @@ unsafe impl WorldQuery for Has { } #[inline] - 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, 's>, + state: &'s Self::State, + table: &'w Table, + ) { *fetch = table.has_column(*state); } @@ -2300,18 +2482,20 @@ unsafe impl WorldQuery for Has { unsafe impl QueryData for Has { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = bool; + type Item<'w, 's> = bool; - 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 } #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { *fetch } } @@ -2319,6 +2503,12 @@ unsafe impl QueryData for Has { /// SAFETY: [`Has`] is read only unsafe impl ReadOnlyQueryData for Has {} +impl ReleaseStateQueryData for Has { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The `AnyOf` query parameter fetches entities with any of the component types included in T. /// /// `Query>` is equivalent to `Query<(Option<&A>, Option<&B>, Option<&mut C>), Or<(With, With, With)>>`. @@ -2327,7 +2517,7 @@ unsafe impl ReadOnlyQueryData for Has {} pub struct AnyOf(PhantomData); macro_rules! impl_tuple_query_data { - ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { + ($(#[$meta:meta])* $(($name: ident, $item: ident)),*) => { #[expect( clippy::allow_attributes, reason = "This is a tuple-related macro; as such the lints below may not always apply." @@ -2349,9 +2539,9 @@ macro_rules! impl_tuple_query_data { unsafe impl<$($name: QueryData),*> QueryData for ($($name,)*) { const IS_READ_ONLY: bool = true $(&& $name::IS_READ_ONLY)*; type ReadOnly = ($($name::ReadOnly,)*); - type Item<'w> = ($($name::Item<'w>,)*); + type Item<'w, 's> = ($($name::Item<'w, 's>,)*); - 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> { let ($($name,)*) = item; ($( $name::shrink($name), @@ -2369,26 +2559,38 @@ macro_rules! impl_tuple_query_data { } #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, table_row: TableRow - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let ($($name,)*) = fetch; // SAFETY: The invariants are upheld by the caller. ($(unsafe { $name::fetch($name, entity, table_row) },)*) } } - $(#[$meta])* /// SAFETY: each item in the tuple is read only unsafe impl<$($name: ReadOnlyQueryData),*> ReadOnlyQueryData for ($($name,)*) {} + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples will generate some function bodies equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." + )] + impl<$($name: ReleaseStateQueryData),*> ReleaseStateQueryData for ($($name,)*) { + fn release_state<'w>(($($item,)*): Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + ($($name::release_state($item),)*) + } + } }; } macro_rules! impl_anytuple_fetch { - ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { + ($(#[$meta:meta])* $(($name: ident, $state: ident, $item: ident)),*) => { $(#[$meta])* #[expect( clippy::allow_attributes, @@ -2412,10 +2614,10 @@ macro_rules! impl_anytuple_fetch { /// `update_component_access` replaces the filters with a disjunction where every element is a conjunction of the previous filters and the filters of one of the subqueries. /// This is sound because `matches_component_set` returns a disjunction of the results of the subqueries' implementations. unsafe impl<$($name: WorldQuery),*> WorldQuery for AnyOf<($($name,)*)> { - type Fetch<'w> = ($(($name::Fetch<'w>, bool),)*); + type Fetch<'w, 's> = ($(($name::Fetch<'w, 's>, bool),)*); type State = ($($name::State,)*); - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>(fetch: Self::Fetch<'wlong, 's>) -> Self::Fetch<'wshort, 's> { let ($($name,)*) = fetch; ($( ($name::shrink_fetch($name.0), $name.1), @@ -2423,7 +2625,7 @@ macro_rules! impl_anytuple_fetch { } #[inline] - unsafe fn init_fetch<'w>(_world: UnsafeWorldCell<'w>, state: &Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w, 's>(_world: UnsafeWorldCell<'w>, state: &'s Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w, 's> { let ($($name,)*) = state; // SAFETY: The invariants are upheld by the caller. ($(( unsafe { $name::init_fetch(_world, $name, _last_run, _this_run) }, false),)*) @@ -2432,9 +2634,9 @@ macro_rules! impl_anytuple_fetch { const IS_DENSE: bool = true $(&& $name::IS_DENSE)*; #[inline] - unsafe fn set_archetype<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table ) { @@ -2450,7 +2652,7 @@ macro_rules! impl_anytuple_fetch { } #[inline] - 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, 's>, _state: &'s Self::State, _table: &'w Table) { let ($($name,)*) = _fetch; let ($($state,)*) = _state; $( @@ -2522,9 +2724,9 @@ macro_rules! impl_anytuple_fetch { unsafe impl<$($name: QueryData),*> QueryData for AnyOf<($($name,)*)> { const IS_READ_ONLY: bool = true $(&& $name::IS_READ_ONLY)*; type ReadOnly = AnyOf<($($name::ReadOnly,)*)>; - type Item<'w> = ($(Option<$name::Item<'w>>,)*); + type Item<'w, 's> = ($(Option<$name::Item<'w, 's>>,)*); - 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> { let ($($name,)*) = item; ($( $name.map($name::shrink), @@ -2532,11 +2734,11 @@ macro_rules! impl_anytuple_fetch { } #[inline(always)] - unsafe fn fetch<'w>( - _fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, _entity: Entity, _table_row: TableRow - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let ($($name,)*) = _fetch; ($( // SAFETY: The invariants are required to be upheld by the caller. @@ -2548,6 +2750,20 @@ macro_rules! impl_anytuple_fetch { $(#[$meta])* /// SAFETY: each item in the tuple is read only unsafe impl<$($name: ReadOnlyQueryData),*> ReadOnlyQueryData for AnyOf<($($name,)*)> {} + + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples will generate some function bodies equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." + )] + impl<$($name: ReleaseStateQueryData),*> ReleaseStateQueryData for AnyOf<($($name,)*)> { + fn release_state<'w>(($($item,)*): Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + ($($item.map(|$item| $name::release_state($item)),)*) + } + } }; } @@ -2557,7 +2773,7 @@ all_tuples!( 0, 15, F, - S + i ); all_tuples!( #[doc(fake_variadic)] @@ -2565,7 +2781,8 @@ all_tuples!( 0, 15, F, - S + S, + i ); /// [`WorldQuery`] used to nullify queries by turning `Query` into `Query>` @@ -2577,10 +2794,13 @@ pub(crate) struct NopWorldQuery(PhantomData); /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for NopWorldQuery { - type Fetch<'w> = (); + type Fetch<'w, 's> = (); type State = D::State; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: ()) {} + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + _fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { + } #[inline(always)] unsafe fn init_fetch( @@ -2627,54 +2847,67 @@ unsafe impl WorldQuery for NopWorldQuery { unsafe impl QueryData for NopWorldQuery { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = (); + type Item<'w, 's> = (); - fn shrink<'wlong: 'wshort, 'wshort>(_: ()) {} + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } #[inline(always)] - unsafe fn fetch<'w>( - _fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { } } /// SAFETY: `NopFetch` never accesses any data unsafe impl ReadOnlyQueryData for NopWorldQuery {} +impl ReleaseStateQueryData for NopWorldQuery { + fn release_state<'w>(_item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> {} +} + /// SAFETY: /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for PhantomData { - type Fetch<'a> = (); + type Fetch<'w, 's> = (); type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort>(_fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + _fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { } - 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> { + ) -> Self::Fetch<'w, 's> { } // `PhantomData` does not match any components, so all components it matches // are stored in a Table (vacuous truth). const IS_DENSE: bool = true; - unsafe fn set_archetype<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table, ) { } - 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, 's>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -2697,21 +2930,28 @@ unsafe impl WorldQuery for PhantomData { unsafe impl QueryData for PhantomData { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'a> = (); + type Item<'w, 's> = (); - 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> { + } - unsafe fn fetch<'w>( - _fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { } } /// SAFETY: `PhantomData` never accesses any world data. unsafe impl ReadOnlyQueryData for PhantomData {} +impl ReleaseStateQueryData for PhantomData { + fn release_state<'w>(_item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> {} +} + /// A compile-time checked union of two different types that differs based on the /// [`StorageType`] of a given component. pub(super) union StorageSwitch { @@ -2828,6 +3068,123 @@ mod tests { assert_is_system(ignored_system); } + #[test] + fn derive_release_state() { + struct NonReleaseQueryData; + + /// SAFETY: + /// `update_component_access` and `update_archetype_component_access` do nothing. + /// This is sound because `fetch` does not access components. + unsafe impl WorldQuery for NonReleaseQueryData { + type Fetch<'w, 's> = (); + type State = (); + + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + _: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { + } + + unsafe fn init_fetch<'w, 's>( + _world: UnsafeWorldCell<'w>, + _state: &'s Self::State, + _last_run: Tick, + _this_run: Tick, + ) -> Self::Fetch<'w, 's> { + } + + const IS_DENSE: bool = true; + + #[inline] + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + _archetype: &'w Archetype, + _table: &Table, + ) { + } + + #[inline] + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + _table: &'w Table, + ) { + } + + fn update_component_access( + _state: &Self::State, + _access: &mut FilteredAccess, + ) { + } + + fn init_state(_world: &mut World) {} + + fn get_state(_components: &Components) -> Option<()> { + Some(()) + } + + fn matches_component_set( + _state: &Self::State, + _set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + true + } + } + + /// SAFETY: `Self` is the same as `Self::ReadOnly` + unsafe impl QueryData for NonReleaseQueryData { + type ReadOnly = Self; + const IS_READ_ONLY: bool = true; + + type Item<'w, 's> = (); + + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } + + #[inline(always)] + unsafe fn fetch<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _entity: Entity, + _table_row: TableRow, + ) -> Self::Item<'w, 's> { + } + } + + /// SAFETY: access is read only + unsafe impl ReadOnlyQueryData for NonReleaseQueryData {} + + #[derive(QueryData)] + pub struct DerivedNonReleaseRead { + non_release: NonReleaseQueryData, + a: &'static A, + } + + #[derive(QueryData)] + #[query_data(mutable)] + pub struct DerivedNonReleaseMutable { + non_release: NonReleaseQueryData, + a: &'static mut A, + } + + #[derive(QueryData)] + pub struct DerivedReleaseRead { + a: &'static A, + } + + #[derive(QueryData)] + #[query_data(mutable)] + pub struct DerivedReleaseMutable { + a: &'static mut A, + } + + fn assert_is_release_state() {} + + assert_is_release_state::(); + assert_is_release_state::(); + } + // Ensures that each field of a `WorldQuery` struct's read-only variant // has the same visibility as its corresponding mutable field. #[test] diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index eccc819ca7..a75acf3e97 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -103,7 +103,7 @@ pub unsafe trait QueryFilter: WorldQuery { /// Must always be called _after_ [`WorldQuery::set_table`] or [`WorldQuery::set_archetype`]. `entity` and /// `table_row` must be in the range of the current table and archetype. unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_>, + fetch: &mut Self::Fetch<'_, '_>, entity: Entity, table_row: TableRow, ) -> bool; @@ -144,10 +144,13 @@ pub struct With(PhantomData); /// `update_component_access` adds a `With` filter for `T`. /// This is sound because `matches_component_set` returns whether the set contains the component. unsafe impl WorldQuery for With { - type Fetch<'w> = (); + type Fetch<'w, 's> = (); type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + _: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { + } #[inline] unsafe fn init_fetch( @@ -204,7 +207,7 @@ unsafe impl QueryFilter for With { #[inline(always)] unsafe fn filter_fetch( - _fetch: &mut Self::Fetch<'_>, + _fetch: &mut Self::Fetch<'_, '_>, _entity: Entity, _table_row: TableRow, ) -> bool { @@ -244,10 +247,13 @@ pub struct Without(PhantomData); /// `update_component_access` adds a `Without` filter for `T`. /// This is sound because `matches_component_set` returns whether the set does not contain the component. unsafe impl WorldQuery for Without { - type Fetch<'w> = (); + type Fetch<'w, 's> = (); type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + _: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { + } #[inline] unsafe fn init_fetch( @@ -304,7 +310,7 @@ unsafe impl QueryFilter for Without { #[inline(always)] unsafe fn filter_fetch( - _fetch: &mut Self::Fetch<'_>, + _fetch: &mut Self::Fetch<'_, '_>, _entity: Entity, _table_row: TableRow, ) -> bool { @@ -345,12 +351,12 @@ unsafe impl QueryFilter for Without { pub struct Or(PhantomData); #[doc(hidden)] -pub struct OrFetch<'w, T: WorldQuery> { - fetch: T::Fetch<'w>, +pub struct OrFetch<'w, 's, T: WorldQuery> { + fetch: T::Fetch<'w, 's>, matches: bool, } -impl Clone for OrFetch<'_, T> { +impl Clone for OrFetch<'_, '_, T> { fn clone(&self) -> Self { Self { fetch: self.fetch.clone(), @@ -384,10 +390,10 @@ macro_rules! impl_or_query_filter { /// `update_component_access` replace the filters with a disjunction where every element is a conjunction of the previous filters and the filters of one of the subqueries. /// This is sound because `matches_component_set` returns a disjunction of the results of the subqueries' implementations. unsafe impl<$($filter: QueryFilter),*> WorldQuery for Or<($($filter,)*)> { - type Fetch<'w> = ($(OrFetch<'w, $filter>,)*); + type Fetch<'w, 's> = ($(OrFetch<'w, 's, $filter>,)*); type State = ($($filter::State,)*); - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>(fetch: Self::Fetch<'wlong, 's>) -> Self::Fetch<'wshort, 's> { let ($($filter,)*) = fetch; ($( OrFetch { @@ -400,7 +406,7 @@ macro_rules! impl_or_query_filter { const IS_DENSE: bool = true $(&& $filter::IS_DENSE)*; #[inline] - unsafe fn init_fetch<'w>(world: UnsafeWorldCell<'w>, state: &Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w, 's>(world: UnsafeWorldCell<'w>, state: &'s Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w, 's> { let ($($filter,)*) = state; ($(OrFetch { // SAFETY: The invariants are upheld by the caller. @@ -410,7 +416,7 @@ macro_rules! impl_or_query_filter { } #[inline] - 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, 's>, state: &'s Self::State, table: &'w Table) { let ($($filter,)*) = fetch; let ($($state,)*) = state; $( @@ -423,9 +429,9 @@ macro_rules! impl_or_query_filter { } #[inline] - unsafe fn set_archetype<'w>( - fetch: &mut Self::Fetch<'w>, - state: & Self::State, + unsafe fn set_archetype<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table ) { @@ -495,7 +501,7 @@ macro_rules! impl_or_query_filter { #[inline(always)] unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_>, + fetch: &mut Self::Fetch<'_, '_>, entity: Entity, table_row: TableRow ) -> bool { @@ -528,7 +534,7 @@ macro_rules! impl_tuple_query_filter { #[inline(always)] unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_>, + fetch: &mut Self::Fetch<'_, '_>, entity: Entity, table_row: TableRow ) -> bool { @@ -568,10 +574,13 @@ pub struct Allows(PhantomData); /// `update_component_access` adds an archetypal filter for `T`. /// This is sound because it doesn't affect the query unsafe impl WorldQuery for Allows { - type Fetch<'w> = (); + type Fetch<'w, 's> = (); type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + _: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { + } #[inline] unsafe fn init_fetch(_: UnsafeWorldCell, _: &ComponentId, _: Tick, _: Tick) {} @@ -609,7 +618,7 @@ unsafe impl QueryFilter for Allows { const IS_ARCHETYPAL: bool = true; #[inline(always)] - unsafe fn filter_fetch(_: &mut Self::Fetch<'_>, _: Entity, _: TableRow) -> bool { + unsafe fn filter_fetch(_: &mut Self::Fetch<'_, '_>, _: Entity, _: TableRow) -> bool { true } } @@ -710,21 +719,23 @@ impl Clone for AddedFetch<'_, T> { /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl WorldQuery for Added { - type Fetch<'w> = AddedFetch<'w, T>; + type Fetch<'w, 's> = AddedFetch<'w, T>; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - &id: &ComponentId, + &id: &'s ComponentId, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w> { - Self::Fetch::<'w> { + ) -> Self::Fetch<'w, 's> { + Self::Fetch::<'w, 's> { ticks: StorageSwitch::new( || None, || { @@ -748,9 +759,9 @@ unsafe impl WorldQuery for Added { }; #[inline] - unsafe fn set_archetype<'w>( - fetch: &mut Self::Fetch<'w>, - component_id: &ComponentId, + unsafe fn set_archetype<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + component_id: &'s ComponentId, _archetype: &'w Archetype, table: &'w Table, ) { @@ -763,9 +774,9 @@ unsafe impl WorldQuery for Added { } #[inline] - unsafe fn set_table<'w>( - fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + &component_id: &'s ComponentId, table: &'w Table, ) { let table_ticks = Some( @@ -807,7 +818,7 @@ unsafe impl QueryFilter for Added { const IS_ARCHETYPAL: bool = false; #[inline(always)] unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_>, + fetch: &mut Self::Fetch<'_, '_>, entity: Entity, table_row: TableRow, ) -> bool { @@ -936,21 +947,23 @@ impl Clone for ChangedFetch<'_, T> { /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl WorldQuery for Changed { - type Fetch<'w> = ChangedFetch<'w, T>; + type Fetch<'w, 's> = ChangedFetch<'w, T>; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - &id: &ComponentId, + &id: &'s ComponentId, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w> { - Self::Fetch::<'w> { + ) -> Self::Fetch<'w, 's> { + Self::Fetch::<'w, 's> { ticks: StorageSwitch::new( || None, || { @@ -974,9 +987,9 @@ unsafe impl WorldQuery for Changed { }; #[inline] - unsafe fn set_archetype<'w>( - fetch: &mut Self::Fetch<'w>, - component_id: &ComponentId, + unsafe fn set_archetype<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + component_id: &'s ComponentId, _archetype: &'w Archetype, table: &'w Table, ) { @@ -989,9 +1002,9 @@ unsafe impl WorldQuery for Changed { } #[inline] - unsafe fn set_table<'w>( - fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + &component_id: &'s ComponentId, table: &'w Table, ) { let table_ticks = Some( @@ -1034,7 +1047,7 @@ unsafe impl QueryFilter for Changed { #[inline(always)] unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_>, + fetch: &mut Self::Fetch<'_, '_>, entity: Entity, table_row: TableRow, ) -> bool { @@ -1133,20 +1146,22 @@ pub struct SpawnedFetch<'w> { // SAFETY: WorldQuery impl accesses no components or component ticks unsafe impl WorldQuery for Spawned { - type Fetch<'w> = SpawnedFetch<'w>; + type Fetch<'w, 's> = SpawnedFetch<'w>; type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &(), + _state: &'s (), last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w> { + ) -> Self::Fetch<'w, 's> { SpawnedFetch { entities: world.entities(), last_run, @@ -1157,16 +1172,21 @@ unsafe impl WorldQuery for Spawned { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &(), + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s (), _archetype: &'w Archetype, _table: &'w Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &(), _table: &'w Table) {} + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s (), + _table: &'w Table, + ) { + } #[inline] fn update_component_access(_state: &(), _access: &mut FilteredAccess) {} @@ -1188,7 +1208,7 @@ unsafe impl QueryFilter for Spawned { #[inline(always)] unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_>, + fetch: &mut Self::Fetch<'_, '_>, entity: Entity, _table_row: TableRow, ) -> bool { diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index baf0d72697..a3b3d02c24 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -140,7 +140,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { range: Option>, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if self.cursor.is_dense { // SAFETY: `self.cursor.is_dense` is true, so storage ids are guaranteed to be table ids. @@ -203,7 +203,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { rows: Range, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if table.is_empty() { return accum; @@ -255,7 +255,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { indices: Range, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if archetype.is_empty() { return accum; @@ -324,7 +324,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { rows: Range, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if archetype.is_empty() { return accum; @@ -492,7 +492,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort()) } @@ -549,7 +549,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort_unstable()) } @@ -605,7 +605,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// ``` pub fn sort_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedIter< 'w, 's, @@ -637,7 +637,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. pub fn sort_unstable_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedIter< 'w, 's, @@ -729,7 +729,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// ``` pub fn sort_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedIter< 'w, 's, @@ -762,7 +762,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. pub fn sort_unstable_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedIter< 'w, 's, @@ -797,7 +797,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. pub fn sort_by_cached_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedIter< 'w, 's, @@ -827,7 +827,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. fn sort_impl( self, - f: impl FnOnce(&mut Vec<(L::Item<'_>, NeutralOrd)>), + f: impl FnOnce(&mut Vec<(L::Item<'_, '_>, NeutralOrd)>), ) -> QuerySortedIter< 'w, 's, @@ -856,7 +856,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { .map(|(key, entity)| (key, NeutralOrd(entity))) .collect(); f(&mut keyed_query); - let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity.0); + let entity_iter = keyed_query + .into_iter() + .map(|(.., entity)| entity.0) + .collect::>() + .into_iter(); // SAFETY: // `self.world` has permission to access the required components. // Each lens query item is dropped before the respective actual query item is accessed. @@ -873,7 +877,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { } impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -975,7 +979,7 @@ where entities: &'w Entities, tables: &'w Tables, archetypes: &'w Archetypes, - fetch: D::Fetch<'w>, + fetch: D::Fetch<'w, 's>, query_state: &'s QueryState, } @@ -1010,7 +1014,7 @@ where /// # Safety /// `entity` must stem from `self.entity_iter`, and not have been passed before. #[inline(always)] - unsafe fn fetch_next(&mut self, entity: Entity) -> D::Item<'w> { + unsafe fn fetch_next(&mut self, entity: Entity) -> D::Item<'w, 's> { let (location, archetype, table); // SAFETY: // `tables` and `archetypes` belong to the same world that the [`QueryIter`] @@ -1048,7 +1052,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> Iterator where I: Iterator, { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1119,8 +1123,8 @@ pub struct QueryManyIter<'w, 's, D: QueryData, F: QueryFilter, I: Iterator, - filter: F::Fetch<'w>, + fetch: D::Fetch<'w, 's>, + filter: F::Fetch<'w, 's>, query_state: &'s QueryState, } @@ -1167,10 +1171,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> entities: &'w Entities, tables: &'w Tables, archetypes: &'w Archetypes, - fetch: &mut D::Fetch<'w>, - filter: &mut F::Fetch<'w>, + fetch: &mut D::Fetch<'w, 's>, + filter: &mut F::Fetch<'w, 's>, query_state: &'s QueryState, - ) -> Option> { + ) -> Option> { for entity_borrow in entity_iter { let entity = entity_borrow.entity(); let Some(location) = entities.get(entity) else { @@ -1212,7 +1216,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// Get next result from the query #[inline(always)] - pub fn fetch_next(&mut self) -> Option> { + pub fn fetch_next(&mut self) -> Option> { // SAFETY: // All arguments stem from self. // We are limiting the returned reference to self, @@ -1336,7 +1340,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort()) } @@ -1394,7 +1398,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort_unstable()) } @@ -1451,7 +1455,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// ``` pub fn sort_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedManyIter< 'w, 's, @@ -1482,7 +1486,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. pub fn sort_unstable_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedManyIter< 'w, 's, @@ -1576,7 +1580,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// ``` pub fn sort_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedManyIter< 'w, 's, @@ -1608,7 +1612,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. pub fn sort_unstable_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedManyIter< 'w, 's, @@ -1642,7 +1646,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. pub fn sort_by_cached_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedManyIter< 'w, 's, @@ -1671,7 +1675,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. fn sort_impl( self, - f: impl FnOnce(&mut Vec<(L::Item<'_>, NeutralOrd)>), + f: impl FnOnce(&mut Vec<(L::Item<'_, '_>, NeutralOrd)>), ) -> QuerySortedManyIter< 'w, 's, @@ -1721,7 +1725,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator Option> { + pub fn fetch_next_back(&mut self) -> Option> { // SAFETY: // All arguments stem from self. // We are limiting the returned reference to self, @@ -1745,7 +1749,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator> Iterator for QueryManyIter<'w, 's, D, F, I> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1861,7 +1865,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: EntitySetIterator> impl<'w, 's, D: QueryData, F: QueryFilter, I: EntitySetIterator> Iterator for QueryManyUniqueIter<'w, 's, D, F, I> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1915,7 +1919,7 @@ pub struct QuerySortedManyIter<'w, 's, D: QueryData, F: QueryFilter, I: Iterator entities: &'w Entities, tables: &'w Tables, archetypes: &'w Archetypes, - fetch: D::Fetch<'w>, + fetch: D::Fetch<'w, 's>, query_state: &'s QueryState, } @@ -1954,7 +1958,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// It is always safe for shared access. /// `entity` must stem from `self.entity_iter`, and not have been passed before. #[inline(always)] - unsafe fn fetch_next_aliased_unchecked(&mut self, entity: Entity) -> D::Item<'w> { + unsafe fn fetch_next_aliased_unchecked(&mut self, entity: Entity) -> D::Item<'w, 's> { let (location, archetype, table); // SAFETY: // `tables` and `archetypes` belong to the same world that the [`QueryIter`] @@ -1988,7 +1992,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// Get next result from the query #[inline(always)] - pub fn fetch_next(&mut self) -> Option> { + pub fn fetch_next(&mut self) -> Option> { let entity = self.entity_iter.next()?; // SAFETY: @@ -2007,7 +2011,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator { /// Get next result from the query #[inline(always)] - pub fn fetch_next_back(&mut self) -> Option> { + pub fn fetch_next_back(&mut self) -> Option> { let entity = self.entity_iter.next_back()?; // SAFETY: @@ -2024,7 +2028,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, I: Iterator> Iterator for QuerySortedManyIter<'w, 's, D, F, I> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -2185,7 +2189,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< /// . /// It is always safe for shared access. #[inline] - unsafe fn fetch_next_aliased_unchecked(&mut self) -> Option<[D::Item<'w>; K]> { + unsafe fn fetch_next_aliased_unchecked(&mut self) -> Option<[D::Item<'w, 's>; K]> { // PERF: can speed up the following code using `cursor.remaining()` instead of `next_item.is_none()` // when D::IS_ARCHETYPAL && F::IS_ARCHETYPAL // @@ -2211,9 +2215,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< } } - let mut values = MaybeUninit::<[D::Item<'w>; K]>::uninit(); + let mut values = MaybeUninit::<[D::Item<'w, 's>; K]>::uninit(); - let ptr = values.as_mut_ptr().cast::>(); + let ptr = values.as_mut_ptr().cast::>(); for (offset, cursor) in self.cursors.iter_mut().enumerate() { ptr.add(offset).write(cursor.peek_last().unwrap()); } @@ -2223,7 +2227,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< /// Get next combination of queried components #[inline] - pub fn fetch_next(&mut self) -> Option<[D::Item<'_>; K]> { + pub fn fetch_next(&mut self) -> Option<[D::Item<'_, 's>; K]> { // SAFETY: we are limiting the returned reference to self, // making sure this method cannot be called multiple times without getting rid // of any previously returned unique references first, thus preventing aliasing. @@ -2240,7 +2244,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, const K: usize> Iterator for QueryCombinationIter<'w, 's, D, F, K> { - type Item = [D::Item<'w>; K]; + type Item = [D::Item<'w, 's>; K]; #[inline] fn next(&mut self) -> Option { @@ -2309,8 +2313,8 @@ struct QueryIterationCursor<'w, 's, D: QueryData, F: QueryFilter> { storage_id_iter: core::slice::Iter<'s, StorageId>, table_entities: &'w [Entity], archetype_entities: &'w [ArchetypeEntity], - fetch: D::Fetch<'w>, - filter: F::Fetch<'w>, + fetch: D::Fetch<'w, 's>, + filter: F::Fetch<'w, 's>, // length of the table or length of the archetype, depending on whether both `D`'s and `F`'s fetches are dense current_len: u32, // either table row or archetype index, depending on whether both `D`'s and `F`'s fetches are dense @@ -2390,7 +2394,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { /// The result of `next` and any previous calls to `peek_last` with this row must have been /// dropped to prevent aliasing mutable references. #[inline] - unsafe fn peek_last(&mut self) -> Option> { + unsafe fn peek_last(&mut self) -> Option> { if self.current_row > 0 { let index = self.current_row - 1; if self.is_dense { @@ -2457,7 +2461,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { tables: &'w Tables, archetypes: &'w Archetypes, query_state: &'s QueryState, - ) -> Option> { + ) -> Option> { if self.is_dense { loop { // we are on the beginning of the query, or finished processing a table, so skip to the next diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index eb5e001e4d..db22c152f7 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -819,34 +819,37 @@ mod tests { /// SAFETY: /// `update_component_access` adds resource read access for `R`. unsafe impl WorldQuery for ReadsRData { - type Fetch<'w> = (); + type Fetch<'w, 's> = (); type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + _: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { + } - 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> { + ) -> Self::Fetch<'w, 's> { } const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, _table: &'w Table, ) { } @@ -882,16 +885,19 @@ mod tests { unsafe impl QueryData for ReadsRData { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = (); + type Item<'w, 's> = (); - 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> { + } #[inline(always)] - unsafe fn fetch<'w>( - _fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + _fetch: &mut Self::Fetch<'w, 's>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { } } diff --git a/crates/bevy_ecs/src/query/par_iter.rs b/crates/bevy_ecs/src/query/par_iter.rs index c685659260..b8d8618fa5 100644 --- a/crates/bevy_ecs/src/query/par_iter.rs +++ b/crates/bevy_ecs/src/query/par_iter.rs @@ -39,7 +39,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[inline] - pub fn for_each) + Send + Sync + Clone>(self, func: FN) { + pub fn for_each) + Send + Sync + Clone>(self, func: FN) { self.for_each_init(|| {}, |_, item| func(item)); } @@ -76,7 +76,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { #[inline] pub fn for_each_init(self, init: INIT, func: FN) where - FN: Fn(&mut T, QueryItem<'w, D>) + Send + Sync + Clone, + FN: Fn(&mut T, QueryItem<'w, 's, D>) + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { let func = |mut init, item| { @@ -190,7 +190,7 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, E: EntityEquivalent + Sync> /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[inline] - pub fn for_each) + Send + Sync + Clone>(self, func: FN) { + pub fn for_each) + Send + Sync + Clone>(self, func: FN) { self.for_each_init(|| {}, |_, item| func(item)); } @@ -247,7 +247,7 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, E: EntityEquivalent + Sync> #[inline] pub fn for_each_init(self, init: INIT, func: FN) where - FN: Fn(&mut T, QueryItem<'w, D>) + Send + Sync + Clone, + FN: Fn(&mut T, QueryItem<'w, 's, D>) + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { let func = |mut init, item| { @@ -345,7 +345,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, E: EntityEquivalent + Sync> /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[inline] - pub fn for_each) + Send + Sync + Clone>(self, func: FN) { + pub fn for_each) + Send + Sync + Clone>(self, func: FN) { self.for_each_init(|| {}, |_, item| func(item)); } @@ -402,7 +402,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, E: EntityEquivalent + Sync> #[inline] pub fn for_each_init(self, init: INIT, func: FN) where - FN: Fn(&mut T, QueryItem<'w, D>) + Send + Sync + Clone, + FN: Fn(&mut T, QueryItem<'w, 's, D>) + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { let func = |mut init, item| { diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 836fefbdfd..005e324cbb 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -845,13 +845,17 @@ impl QueryState { /// /// This can only be called for read-only queries, see [`Self::get_mut`] for write-queries. /// + /// If you need to get multiple items at once but get borrowing errors, + /// consider using [`Self::update_archetypes`] followed by multiple [`Self::get_manual`] calls, + /// or making a single call with [`Self::get_many`] or [`Self::iter_many`]. + /// /// This is always guaranteed to run in `O(1)` time. #[inline] pub fn get<'w>( &mut self, world: &'w World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query(world).get_inner(entity) } @@ -892,7 +896,7 @@ impl QueryState { &mut self, world: &'w World, entities: [Entity; N], - ) -> Result<[ROQueryItem<'w, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'w, '_, D>; N], QueryEntityError> { self.query(world).get_many_inner(entities) } @@ -930,7 +934,7 @@ impl QueryState { &mut self, world: &'w World, entities: UniqueEntityArray, - ) -> Result<[ROQueryItem<'w, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'w, '_, D>; N], QueryEntityError> { self.query(world).get_many_unique_inner(entities) } @@ -942,7 +946,7 @@ impl QueryState { &mut self, world: &'w mut World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_mut(world).get_inner(entity) } @@ -989,7 +993,7 @@ impl QueryState { &mut self, world: &'w mut World, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, '_>; N], QueryEntityError> { self.query_mut(world).get_many_mut_inner(entities) } @@ -1034,7 +1038,7 @@ impl QueryState { &mut self, world: &'w mut World, entities: UniqueEntityArray, - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, '_>; N], QueryEntityError> { self.query_mut(world).get_many_unique_inner(entities) } @@ -1056,7 +1060,7 @@ impl QueryState { &self, world: &'w World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_manual(world).get_inner(entity) } @@ -1073,13 +1077,16 @@ impl QueryState { &mut self, world: UnsafeWorldCell<'w>, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_unchecked(world).get_inner(entity) } /// Returns an [`Iterator`] over the query results for the given [`World`]. /// /// This can only be called for read-only queries, see [`Self::iter_mut`] for write-queries. + /// + /// If you need to iterate multiple times at once but get borrowing errors, + /// consider using [`Self::update_archetypes`] followed by multiple [`Self::iter_manual`] calls. #[inline] pub fn iter<'w, 's>(&'s mut self, world: &'w World) -> QueryIter<'w, 's, D::ReadOnly, F> { self.query(world).into_iter() @@ -1168,6 +1175,9 @@ impl QueryState { /// Items are returned in the order of the list of entities. /// Entities that don't match the query are skipped. /// + /// If you need to iterate multiple times at once but get borrowing errors, + /// consider using [`Self::update_archetypes`] followed by multiple [`Self::iter_many_manual`] calls. + /// /// # See also /// /// - [`iter_many_mut`](Self::iter_many_mut) to get mutable query items. @@ -1387,8 +1397,8 @@ impl QueryState { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - pub(crate) unsafe fn par_fold_init_unchecked_manual<'w, T, FN, INIT>( - &self, + pub(crate) unsafe fn par_fold_init_unchecked_manual<'w, 's, T, FN, INIT>( + &'s self, init_accum: INIT, world: UnsafeWorldCell<'w>, batch_size: u32, @@ -1396,7 +1406,7 @@ impl QueryState { last_run: Tick, this_run: Tick, ) where - FN: Fn(T, D::Item<'w>) -> T + Send + Sync + Clone, + FN: Fn(T, D::Item<'w, 's>) -> T + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: @@ -1501,8 +1511,8 @@ impl QueryState { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - pub(crate) unsafe fn par_many_unique_fold_init_unchecked_manual<'w, T, FN, INIT, E>( - &self, + pub(crate) unsafe fn par_many_unique_fold_init_unchecked_manual<'w, 's, T, FN, INIT, E>( + &'s self, init_accum: INIT, world: UnsafeWorldCell<'w>, entity_list: &UniqueEntityEquivalentSlice, @@ -1511,7 +1521,7 @@ impl QueryState { last_run: Tick, this_run: Tick, ) where - FN: Fn(T, D::Item<'w>) -> T + Send + Sync + Clone, + FN: Fn(T, D::Item<'w, 's>) -> T + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, E: EntityEquivalent + Sync, { @@ -1564,8 +1574,8 @@ impl QueryState { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - pub(crate) unsafe fn par_many_fold_init_unchecked_manual<'w, T, FN, INIT, E>( - &self, + pub(crate) unsafe fn par_many_fold_init_unchecked_manual<'w, 's, T, FN, INIT, E>( + &'s self, init_accum: INIT, world: UnsafeWorldCell<'w>, entity_list: &[E], @@ -1574,7 +1584,7 @@ impl QueryState { last_run: Tick, this_run: Tick, ) where - FN: Fn(T, D::Item<'w>) -> T + Send + Sync + Clone, + FN: Fn(T, D::Item<'w, 's>) -> T + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, E: EntityEquivalent + Sync, { @@ -1686,7 +1696,10 @@ impl QueryState { /// /// Simply unwrapping the [`Result`] also works, but should generally be reserved for tests. #[inline] - pub fn single<'w>(&mut self, world: &'w World) -> Result, QuerySingleError> { + pub fn single<'w>( + &mut self, + world: &'w World, + ) -> Result, QuerySingleError> { self.query(world).single_inner() } @@ -1703,7 +1716,7 @@ impl QueryState { pub fn single_mut<'w>( &mut self, world: &'w mut World, - ) -> Result, QuerySingleError> { + ) -> Result, QuerySingleError> { self.query_mut(world).single_inner() } @@ -1720,7 +1733,7 @@ impl QueryState { pub unsafe fn single_unchecked<'w>( &mut self, world: UnsafeWorldCell<'w>, - ) -> Result, QuerySingleError> { + ) -> Result, QuerySingleError> { self.query_unchecked(world).single_inner() } @@ -1742,7 +1755,7 @@ impl QueryState { world: UnsafeWorldCell<'w>, last_run: Tick, this_run: Tick, - ) -> Result, QuerySingleError> { + ) -> Result, QuerySingleError> { // SAFETY: // - The caller ensured we have the correct access to the world. // - The caller ensured that the world matches. diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index a6bcbf58bd..e856be7619 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -42,7 +42,7 @@ use variadics_please::all_tuples; /// [`QueryFilter`]: crate::query::QueryFilter pub unsafe trait WorldQuery { /// Per archetype/table state retrieved by this [`WorldQuery`] to compute [`Self::Item`](crate::query::QueryData::Item) for each entity. - type Fetch<'a>: Clone; + type Fetch<'w, 's>: Clone; /// State used to construct a [`Self::Fetch`](WorldQuery::Fetch). This will be cached inside [`QueryState`](crate::query::QueryState), /// so it is best to move as much data / computation here as possible to reduce the cost of @@ -50,7 +50,9 @@ pub unsafe trait WorldQuery { type State: Send + Sync + Sized; /// This function manually implements subtyping for the query fetches. - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort>; + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's>; /// Creates a new instance of [`Self::Fetch`](WorldQuery::Fetch), /// by combining data from the [`World`] with the cached [`Self::State`](WorldQuery::State). @@ -62,12 +64,12 @@ pub unsafe trait WorldQuery { /// in to this function. /// - `world` must have the **right** to access any access registered in `update_component_access`. /// - There must not be simultaneous resource access conflicting with readonly resource access registered in [`WorldQuery::update_component_access`]. - 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>; + ) -> Self::Fetch<'w, 's>; /// Returns true if (and only if) every table of every archetype matched by this fetch contains /// all of the matched components. @@ -87,9 +89,9 @@ pub unsafe trait WorldQuery { /// - `archetype` and `tables` must be from the same [`World`] that [`WorldQuery::init_state`] was called on. /// - `table` must correspond to `archetype`. /// - `state` must be the [`State`](Self::State) that `fetch` was initialized with. - unsafe fn set_archetype<'w>( - fetch: &mut Self::Fetch<'w>, - state: &Self::State, + unsafe fn set_archetype<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table, ); @@ -101,7 +103,11 @@ pub unsafe trait WorldQuery { /// /// - `table` must be from the same [`World`] that [`WorldQuery::init_state`] was called on. /// - `state` must be the [`State`](Self::State) that `fetch` was initialized with. - 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, 's>, + state: &'s Self::State, + table: &'w Table, + ); /// Adds any component accesses used by this [`WorldQuery`] to `access`. /// @@ -154,11 +160,11 @@ macro_rules! impl_tuple_world_query { /// `update_component_access` adds all `With` and `Without` filters from the subqueries. /// This is sound because `matches_component_set` always returns `false` if any the subqueries' implementations return `false`. unsafe impl<$($name: WorldQuery),*> WorldQuery for ($($name,)*) { - type Fetch<'w> = ($($name::Fetch<'w>,)*); + type Fetch<'w, 's> = ($($name::Fetch<'w, 's>,)*); type State = ($($name::State,)*); - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>(fetch: Self::Fetch<'wlong, 's>) -> Self::Fetch<'wshort, 's> { let ($($name,)*) = fetch; ($( $name::shrink_fetch($name), @@ -166,7 +172,7 @@ macro_rules! impl_tuple_world_query { } #[inline] - unsafe fn init_fetch<'w>(world: UnsafeWorldCell<'w>, state: &Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w, 's>(world: UnsafeWorldCell<'w>, state: &'s Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w, 's> { let ($($name,)*) = state; // SAFETY: The invariants are upheld by the caller. ($(unsafe { $name::init_fetch(world, $name, last_run, this_run) },)*) @@ -175,9 +181,9 @@ macro_rules! impl_tuple_world_query { const IS_DENSE: bool = true $(&& $name::IS_DENSE)*; #[inline] - unsafe fn set_archetype<'w>( - fetch: &mut Self::Fetch<'w>, - state: &Self::State, + unsafe fn set_archetype<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table ) { @@ -188,7 +194,7 @@ macro_rules! impl_tuple_world_query { } #[inline] - 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, 's>, state: &'s Self::State, table: &'w Table) { let ($($name,)*) = fetch; let ($($state,)*) = state; // SAFETY: The invariants are upheld by the caller. diff --git a/crates/bevy_ecs/src/relationship/relationship_query.rs b/crates/bevy_ecs/src/relationship/relationship_query.rs index a2ec937c29..a7acea7de0 100644 --- a/crates/bevy_ecs/src/relationship/relationship_query.rs +++ b/crates/bevy_ecs/src/relationship/relationship_query.rs @@ -14,7 +14,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// target entity of that relationship. pub fn related(&'w self, entity: Entity) -> Option where - ::ReadOnly: QueryData = &'w R>, + ::ReadOnly: QueryData = &'w R>, { self.get(entity).map(R::get).ok() } @@ -26,7 +26,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> impl Iterator + 'w where - ::ReadOnly: QueryData = &'w S>, + ::ReadOnly: QueryData = &'w S>, { self.get(entity) .into_iter() @@ -42,7 +42,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity. pub fn root_ancestor(&'w self, entity: Entity) -> Entity where - ::ReadOnly: QueryData = &'w R>, + ::ReadOnly: QueryData = &'w R>, { // Recursively search up the tree until we're out of parents match self.get(entity) { @@ -60,9 +60,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn iter_leaves( &'w self, entity: Entity, - ) -> impl Iterator + 'w + ) -> impl Iterator + use<'w, 's, S, D, F> where - ::ReadOnly: QueryData = &'w S>, + ::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { self.iter_descendants_depth_first(entity).filter(|entity| { @@ -80,7 +80,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> impl Iterator + 'w where - D::ReadOnly: QueryData = (Option<&'w R>, Option<&'w R::RelationshipTarget>)>, + D::ReadOnly: QueryData = (Option<&'w R>, Option<&'w R::RelationshipTarget>)>, { self.get(entity) .ok() @@ -103,7 +103,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> DescendantIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { DescendantIter::new(self, entity) } @@ -120,7 +120,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> DescendantDepthFirstIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { DescendantDepthFirstIter::new(self, entity) @@ -137,7 +137,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> AncestorIter<'w, 's, D, F, R> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { AncestorIter::new(self, entity) } @@ -148,7 +148,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Traverses the hierarchy breadth-first. pub struct DescendantIter<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { children_query: &'w Query<'w, 's, D, F>, vecdeque: VecDeque, @@ -156,7 +156,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> DescendantIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { /// Returns a new [`DescendantIter`]. pub fn new(children_query: &'w Query<'w, 's, D, F>, entity: Entity) -> Self { @@ -174,7 +174,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> Iterator for DescendantIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { type Item = Entity; @@ -194,7 +194,7 @@ where /// Traverses the hierarchy depth-first. pub struct DescendantDepthFirstIter<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { children_query: &'w Query<'w, 's, D, F>, stack: SmallVec<[Entity; 8]>, @@ -203,7 +203,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> DescendantDepthFirstIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { /// Returns a new [`DescendantDepthFirstIter`]. @@ -220,7 +220,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> Iterator for DescendantDepthFirstIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { type Item = Entity; @@ -239,7 +239,7 @@ where /// An [`Iterator`] of [`Entity`]s over the ancestors of an [`Entity`]. pub struct AncestorIter<'w, 's, D: QueryData, F: QueryFilter, R: Relationship> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { parent_query: &'w Query<'w, 's, D, F>, next: Option, @@ -247,7 +247,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, R: Relationship> AncestorIter<'w, 's, D, F, R> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { /// Returns a new [`AncestorIter`]. pub fn new(parent_query: &'w Query<'w, 's, D, F>, entity: Entity) -> Self { @@ -261,7 +261,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, R: Relationship> Iterator for AncestorIter<'w, 's, D, F, R> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { type Item = Entity; diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index dfc07d23f1..2664d3a44f 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1185,7 +1185,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// [`par_iter_mut`]: Self::par_iter_mut /// [`World`]: crate::world::World #[inline] - pub fn par_iter(&self) -> QueryParIter<'_, '_, D::ReadOnly, F> { + pub fn par_iter(&self) -> QueryParIter<'_, 's, D::ReadOnly, F> { self.as_readonly().par_iter_inner() } @@ -1220,7 +1220,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// [`par_iter`]: Self::par_iter /// [`World`]: crate::world::World #[inline] - pub fn par_iter_mut(&mut self) -> QueryParIter<'_, '_, D, F> { + pub fn par_iter_mut(&mut self) -> QueryParIter<'_, 's, D, F> { self.reborrow().par_iter_inner() } @@ -1280,7 +1280,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn par_iter_many>( &self, entities: EntityList, - ) -> QueryParManyIter<'_, '_, D::ReadOnly, F, EntityList::Item> { + ) -> QueryParManyIter<'_, 's, D::ReadOnly, F, EntityList::Item> { QueryParManyIter { world: self.world, state: self.state.as_readonly(), @@ -1309,7 +1309,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn par_iter_many_unique>( &self, entities: EntityList, - ) -> QueryParManyUniqueIter<'_, '_, D::ReadOnly, F, EntityList::Item> { + ) -> QueryParManyUniqueIter<'_, 's, D::ReadOnly, F, EntityList::Item> { QueryParManyUniqueIter { world: self.world, state: self.state.as_readonly(), @@ -1338,7 +1338,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn par_iter_many_unique_mut>( &mut self, entities: EntityList, - ) -> QueryParManyUniqueIter<'_, '_, D, F, EntityList::Item> { + ) -> QueryParManyUniqueIter<'_, 's, D, F, EntityList::Item> { QueryParManyUniqueIter { world: self.world, state: self.state, @@ -1383,7 +1383,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get_mut`](Self::get_mut) to get a mutable query item. #[inline] - pub fn get(&self, entity: Entity) -> Result, QueryEntityError> { + pub fn get(&self, entity: Entity) -> Result, QueryEntityError> { self.as_readonly().get_inner(entity) } @@ -1434,7 +1434,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many( &self, entities: [Entity; N], - ) -> Result<[ROQueryItem<'_, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'_, 's, D>; N], QueryEntityError> { // Note that we call a separate `*_inner` method from `get_many_mut` // because we don't need to check for duplicates. self.as_readonly().get_many_inner(entities) @@ -1485,7 +1485,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_unique( &self, entities: UniqueEntityArray, - ) -> Result<[ROQueryItem<'_, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'_, 's, D>; N], QueryEntityError> { self.as_readonly().get_many_unique_inner(entities) } @@ -1519,7 +1519,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get`](Self::get) to get a read-only query item. #[inline] - pub fn get_mut(&mut self, entity: Entity) -> Result, QueryEntityError> { + pub fn get_mut(&mut self, entity: Entity) -> Result, QueryEntityError> { self.reborrow().get_inner(entity) } @@ -1534,7 +1534,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get_mut`](Self::get_mut) to get the item using a mutable borrow of the [`Query`]. #[inline] - pub fn get_inner(self, entity: Entity) -> Result, QueryEntityError> { + pub fn get_inner(self, entity: Entity) -> Result, QueryEntityError> { // SAFETY: system runs without conflicts with other systems. // same-system queries have runtime borrow checks when they conflict unsafe { @@ -1662,7 +1662,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_mut( &mut self, entities: [Entity; N], - ) -> Result<[D::Item<'_>; N], QueryEntityError> { + ) -> Result<[D::Item<'_, 's>; N], QueryEntityError> { self.reborrow().get_many_mut_inner(entities) } @@ -1730,7 +1730,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_unique_mut( &mut self, entities: UniqueEntityArray, - ) -> Result<[D::Item<'_>; N], QueryEntityError> { + ) -> Result<[D::Item<'_, 's>; N], QueryEntityError> { self.reborrow().get_many_unique_inner(entities) } @@ -1749,7 +1749,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_mut_inner( self, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> { // Verify that all entities are unique for i in 0..N { for j in 0..i { @@ -1777,7 +1777,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_inner( self, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> where D: ReadOnlyQueryData, { @@ -1799,7 +1799,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_unique_inner( self, entities: UniqueEntityArray, - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> { // SAFETY: All entities are unique, so the results don't alias. unsafe { self.get_many_impl(entities.into_inner()) } } @@ -1814,7 +1814,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { unsafe fn get_many_impl( self, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> { let mut values = [(); N].map(|_| MaybeUninit::uninit()); for (value, entity) in core::iter::zip(&mut values, entities) { @@ -1842,7 +1842,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get_mut`](Self::get_mut) for the safe version. #[inline] - pub unsafe fn get_unchecked(&self, entity: Entity) -> Result, QueryEntityError> { + pub unsafe fn get_unchecked( + &self, + entity: Entity, + ) -> Result, QueryEntityError> { // SAFETY: The caller promises that this will not result in multiple mutable references. unsafe { self.reborrow_unsafe() }.get_inner(entity) } @@ -1878,7 +1881,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`single_mut`](Self::single_mut) to get the mutable query item. #[inline] - pub fn single(&self) -> Result, QuerySingleError> { + pub fn single(&self) -> Result, QuerySingleError> { self.as_readonly().single_inner() } @@ -1907,7 +1910,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`single`](Self::single) to get the read-only query item. #[inline] - pub fn single_mut(&mut self) -> Result, QuerySingleError> { + pub fn single_mut(&mut self) -> Result, QuerySingleError> { self.reborrow().single_inner() } @@ -1939,7 +1942,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// - [`single_mut`](Self::single_mut) to get the mutable query item. /// - [`single_inner`](Self::single_inner) for the panicking version. #[inline] - pub fn single_inner(self) -> Result, QuerySingleError> { + pub fn single_inner(self) -> Result, QuerySingleError> { let mut query = self.into_iter(); let first = query.next(); let extra = query.next().is_some(); @@ -2451,7 +2454,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { } impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for Query<'w, 's, D, F> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; type IntoIter = QueryIter<'w, 's, D, F>; fn into_iter(self) -> Self::IntoIter { @@ -2464,7 +2467,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for Query<'w, 's, D, F> } impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w Query<'_, 's, D, F> { - type Item = ROQueryItem<'w, D>; + type Item = ROQueryItem<'w, 's, D>; type IntoIter = QueryIter<'w, 's, D::ReadOnly, F>; fn into_iter(self) -> Self::IntoIter { @@ -2473,7 +2476,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w Query<'_, 's, D, } impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w mut Query<'_, 's, D, F> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; type IntoIter = QueryIter<'w, 's, D, F>; fn into_iter(self) -> Self::IntoIter { @@ -2588,28 +2591,28 @@ impl<'w, 'q, Q: QueryData, F: QueryFilter> From<&'q mut Query<'w, '_, Q, F>> /// ``` /// Note that because [`Single`] implements [`Deref`] and [`DerefMut`], methods and fields like `health` can be accessed directly. /// You can also access the underlying data manually, by calling `.deref`/`.deref_mut`, or by using the `*` operator. -pub struct Single<'w, D: QueryData, F: QueryFilter = ()> { - pub(crate) item: D::Item<'w>, +pub struct Single<'w, 's, D: QueryData, F: QueryFilter = ()> { + pub(crate) item: D::Item<'w, 's>, pub(crate) _filter: PhantomData, } -impl<'w, D: QueryData, F: QueryFilter> Deref for Single<'w, D, F> { - type Target = D::Item<'w>; +impl<'w, 's, D: QueryData, F: QueryFilter> Deref for Single<'w, 's, D, F> { + type Target = D::Item<'w, 's>; fn deref(&self) -> &Self::Target { &self.item } } -impl<'w, D: QueryData, F: QueryFilter> DerefMut for Single<'w, D, F> { +impl<'w, 's, D: QueryData, F: QueryFilter> DerefMut for Single<'w, 's, D, F> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.item } } -impl<'w, D: QueryData, F: QueryFilter> Single<'w, D, F> { +impl<'w, 's, D: QueryData, F: QueryFilter> Single<'w, 's, D, F> { /// Returns the inner item with ownership. - pub fn into_inner(self) -> D::Item<'w> { + pub fn into_inner(self) -> D::Item<'w, 's> { self.item } } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 153f85f336..ac2637ed7a 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -389,9 +389,11 @@ fn assert_component_access_compatibility( // SAFETY: Relevant query ComponentId access is applied to SystemMeta. If // this Query conflicts with any prior access, a panic will occur. -unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam for Single<'a, D, F> { +unsafe impl<'a, 'b, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam + for Single<'a, 'b, D, F> +{ type State = QueryState; - type Item<'w, 's> = Single<'w, D, F>; + type Item<'w, 's> = Single<'w, 's, D, F>; fn init_state(world: &mut World) -> Self::State { Query::init_state(world) @@ -451,8 +453,8 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam fo } // SAFETY: QueryState is constrained to read-only fetches, so it only reads World. -unsafe impl<'a, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> ReadOnlySystemParam - for Single<'a, D, F> +unsafe impl<'a, 'b, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> ReadOnlySystemParam + for Single<'a, 'b, D, F> { } diff --git a/crates/bevy_ecs/src/traversal.rs b/crates/bevy_ecs/src/traversal.rs index d6527a10fa..577720fd5d 100644 --- a/crates/bevy_ecs/src/traversal.rs +++ b/crates/bevy_ecs/src/traversal.rs @@ -1,6 +1,10 @@ //! A trait for components that let you traverse the ECS. -use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship}; +use crate::{ + entity::Entity, + query::{ReadOnlyQueryData, ReleaseStateQueryData}, + relationship::Relationship, +}; /// A component that can point to another entity, and which can be used to define a path through the ECS. /// @@ -20,13 +24,13 @@ use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship /// [specify the direction]: crate::event::EntityEvent::Traversal /// [event propagation]: crate::observer::On::propagate /// [observers]: crate::observer::Observer -pub trait Traversal: ReadOnlyQueryData { +pub trait Traversal: ReadOnlyQueryData + ReleaseStateQueryData { /// Returns the next entity to visit. - fn traverse(item: Self::Item<'_>, data: &D) -> Option; + fn traverse(item: Self::Item<'_, '_>, data: &D) -> Option; } impl Traversal for () { - fn traverse(_: Self::Item<'_>, _data: &D) -> Option { + fn traverse(_: Self::Item<'_, '_>, _data: &D) -> Option { None } } @@ -39,7 +43,7 @@ impl Traversal for () { /// /// [event propagation]: crate::observer::On::propagate impl Traversal for &R { - fn traverse(item: Self::Item<'_>, _data: &D) -> Option { + fn traverse(item: Self::Item<'_, '_>, _data: &D) -> Option { Some(item.get()) } } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index ab470efcc7..d29c3db428 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -16,7 +16,7 @@ use crate::{ event::EntityEvent, lifecycle::{DESPAWN, REMOVE, REPLACE}, observer::Observer, - query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData}, + query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, relationship::RelationshipHookMode, resource::Resource, system::IntoObserverSystem, @@ -279,14 +279,16 @@ impl<'w> EntityRef<'w> { /// # Panics /// /// If the entity does not have the components required by the query `Q`. - pub fn components(&self) -> Q::Item<'w> { + pub fn components(&self) -> Q::Item<'w, 'static> { self.get_components::() .expect("Query does not match the current entity") } /// Returns read-only components for the current entity that match the query `Q`, /// or `None` if the entity does not have the components required by the query `Q`. - pub fn get_components(&self) -> Option> { + pub fn get_components( + &self, + ) -> Option> { // SAFETY: We have read-only access to all components of this entity. unsafe { self.cell.get_components::() } } @@ -546,13 +548,15 @@ impl<'w> EntityMut<'w> { /// # Panics /// /// If the entity does not have the components required by the query `Q`. - pub fn components(&self) -> Q::Item<'_> { + pub fn components(&self) -> Q::Item<'_, 'static> { self.as_readonly().components::() } /// Returns read-only components for the current entity that match the query `Q`, /// or `None` if the entity does not have the components required by the query `Q`. - pub fn get_components(&self) -> Option> { + pub fn get_components( + &self, + ) -> Option> { self.as_readonly().get_components::() } @@ -1310,7 +1314,7 @@ impl<'w> EntityWorldMut<'w> { /// If the entity does not have the components required by the query `Q` or if the entity /// has been despawned while this `EntityWorldMut` is still alive. #[inline] - pub fn components(&self) -> Q::Item<'_> { + pub fn components(&self) -> Q::Item<'_, 'static> { self.as_readonly().components::() } @@ -1321,7 +1325,9 @@ impl<'w> EntityWorldMut<'w> { /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] - pub fn get_components(&self) -> Option> { + pub fn get_components( + &self, + ) -> Option> { self.as_readonly().get_components::() } diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 458ea5aa17..6c618a287f 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -11,7 +11,7 @@ use crate::{ lifecycle::RemovedComponentEvents, observer::Observers, prelude::Component, - query::{DebugCheckedUnwrap, ReadOnlyQueryData}, + query::{DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, resource::Resource, storage::{ComponentSparseSet, Storages, Table}, world::RawCommandQueue, @@ -998,7 +998,9 @@ impl<'w> UnsafeEntityCell<'w> { /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the queried data immutably /// - no mutable references to the queried data exist at the same time - pub(crate) unsafe fn get_components(&self) -> Option> { + pub(crate) unsafe fn get_components( + &self, + ) -> Option> { // SAFETY: World is only used to access query data and initialize query state let state = unsafe { let world = self.world().world(); @@ -1028,7 +1030,8 @@ impl<'w> UnsafeEntityCell<'w> { // Table corresponds to archetype. State is the same state used to init fetch above. unsafe { Q::set_archetype(&mut fetch, &state, archetype, table) } // SAFETY: Called after set_archetype above. Entity and location are guaranteed to exist. - unsafe { Some(Q::fetch(&mut fetch, self.id(), location.table_row)) } + let item = unsafe { Q::fetch(&mut fetch, self.id(), location.table_row) }; + Some(Q::release_state(item)) } else { None } diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index de48d94e42..5804729e06 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -631,8 +631,8 @@ impl RenderCommand

for SetLineGizmoBindGroup #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - uniform_index: Option>, + _view: ROQueryItem<'w, '_, Self::ViewQuery>, + uniform_index: Option>, bind_group: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -662,8 +662,8 @@ impl RenderCommand

for DrawLineGizmo #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - config: Option>, + _view: ROQueryItem<'w, '_, Self::ViewQuery>, + config: Option>, line_gizmos: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -725,8 +725,8 @@ impl RenderCommand

for DrawLineJointGizmo { #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - config: Option>, + _view: ROQueryItem<'w, '_, Self::ViewQuery>, + config: Option>, line_gizmos: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 1fe7c4b7ef..cbf88740fd 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -155,7 +155,7 @@ pub struct WindowTraversal { } impl Traversal> for WindowTraversal { - fn traverse(item: Self::Item<'_>, event: &FocusedInput) -> Option { + fn traverse(item: Self::Item<'_, '_>, event: &FocusedInput) -> Option { let WindowTraversalItem { child_of, window } = item; // Send event to parent, if it has one. diff --git a/crates/bevy_pbr/src/atmosphere/mod.rs b/crates/bevy_pbr/src/atmosphere/mod.rs index 8b08751428..a55403630a 100644 --- a/crates/bevy_pbr/src/atmosphere/mod.rs +++ b/crates/bevy_pbr/src/atmosphere/mod.rs @@ -309,7 +309,7 @@ impl ExtractComponent for Atmosphere { type Out = Atmosphere; - fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { Some(item.clone()) } } @@ -405,7 +405,7 @@ impl ExtractComponent for AtmosphereSettings { type Out = AtmosphereSettings; - fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { Some(item.clone()) } } diff --git a/crates/bevy_pbr/src/atmosphere/node.rs b/crates/bevy_pbr/src/atmosphere/node.rs index 851447d760..e09b27c590 100644 --- a/crates/bevy_pbr/src/atmosphere/node.rs +++ b/crates/bevy_pbr/src/atmosphere/node.rs @@ -181,7 +181,7 @@ impl ViewNode for RenderSkyNode { view_uniforms_offset, lights_uniforms_offset, render_sky_pipeline_id, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); diff --git a/crates/bevy_pbr/src/light_probe/environment_map.rs b/crates/bevy_pbr/src/light_probe/environment_map.rs index b31801f757..fa55b4d94a 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.rs +++ b/crates/bevy_pbr/src/light_probe/environment_map.rs @@ -192,7 +192,7 @@ impl ExtractInstance for EnvironmentMapIds { type QueryFilter = (); - fn extract(item: QueryItem<'_, Self::QueryData>) -> Option { + fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option { Some(EnvironmentMapIds { diffuse: item.diffuse_map.id(), specular: item.specular_map.id(), diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 8c408233e1..d8a3256b13 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1540,7 +1540,7 @@ fn extract_mesh_for_gpu_building( not_shadow_caster, no_automatic_batching, visibility_range, - ): ::Item<'_>, + ): ::Item<'_, '_>, render_visibility_ranges: &RenderVisibilityRanges, render_mesh_instances: &RenderMeshInstancesGpu, queue: &mut RenderMeshInstanceGpuQueue, @@ -2874,7 +2874,7 @@ impl RenderCommand

for SetMeshViewBindGroup view_environment_map, mesh_view_bind_group, maybe_oit_layers_count_offset, - ): ROQueryItem<'w, Self::ViewQuery>, + ): ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, _: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, diff --git a/crates/bevy_pbr/src/ssr/mod.rs b/crates/bevy_pbr/src/ssr/mod.rs index 9f7dbb2f76..22d5acd1e9 100644 --- a/crates/bevy_pbr/src/ssr/mod.rs +++ b/crates/bevy_pbr/src/ssr/mod.rs @@ -280,7 +280,7 @@ impl ViewNode for ScreenSpaceReflectionsNode { view_environment_map_offset, view_bind_group, ssr_pipeline_id, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { // Grab the render pipeline. @@ -498,7 +498,7 @@ impl ExtractComponent for ScreenSpaceReflections { type Out = ScreenSpaceReflectionsUniform; - fn extract_component(settings: QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(settings: QueryItem<'_, '_, Self::QueryData>) -> Option { if !DEPTH_TEXTURE_SAMPLING_SUPPORTED { once!(info!( "Disabling screen-space reflections on this platform because depth textures \ diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index 45f694e546..625ff42dc5 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -347,7 +347,7 @@ impl ViewNode for VolumetricFogNode { view_ssr_offset, msaa, view_environment_map_offset, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index cc64ad2e4f..e42e1309ec 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -374,7 +374,7 @@ impl ViewNode for Wireframe3dNode { &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(wireframe_phase) = world.get_resource::>() diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 2116d986af..a7a3979c59 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -86,7 +86,7 @@ impl Traversal> for PointerTraversal where E: Debug + Clone + Reflect, { - fn traverse(item: Self::Item<'_>, pointer: &Pointer) -> Option { + fn traverse(item: Self::Item<'_, '_>, pointer: &Pointer) -> Option { let PointerTraversalItem { child_of, window } = item; // Send event to parent, if it has one. diff --git a/crates/bevy_render/macros/src/extract_component.rs b/crates/bevy_render/macros/src/extract_component.rs index 2bfd0e0e11..8526f7b889 100644 --- a/crates/bevy_render/macros/src/extract_component.rs +++ b/crates/bevy_render/macros/src/extract_component.rs @@ -43,7 +43,7 @@ pub fn derive_extract_component(input: TokenStream) -> TokenStream { type QueryFilter = #filter; type Out = Self; - fn extract_component(item: #bevy_ecs_path::query::QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(item: #bevy_ecs_path::query::QueryItem<'_, '_, Self::QueryData>) -> Option { Some(item.clone()) } } diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index e1f528d6ab..b7bb05e425 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -60,7 +60,7 @@ pub trait ExtractComponent: Component { // type Out: Component = Self; /// Defines how the component is transferred into the "render world". - fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option; + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option; } /// This plugin prepares the components of the corresponding type for the GPU diff --git a/crates/bevy_render/src/extract_instances.rs b/crates/bevy_render/src/extract_instances.rs index a8e5a9ecbd..cd194b7c40 100644 --- a/crates/bevy_render/src/extract_instances.rs +++ b/crates/bevy_render/src/extract_instances.rs @@ -34,7 +34,7 @@ pub trait ExtractInstance: Send + Sync + Sized + 'static { type QueryFilter: QueryFilter; /// Defines how the component is transferred into the "render world". - fn extract(item: QueryItem<'_, Self::QueryData>) -> Option; + fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option; } /// This plugin extracts one or more components into the "render world" as diff --git a/crates/bevy_render/src/render_graph/node.rs b/crates/bevy_render/src/render_graph/node.rs index 0a634c2598..4355892487 100644 --- a/crates/bevy_render/src/render_graph/node.rs +++ b/crates/bevy_render/src/render_graph/node.rs @@ -366,7 +366,7 @@ pub trait ViewNode { &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>; } diff --git a/crates/bevy_render/src/render_phase/draw.rs b/crates/bevy_render/src/render_phase/draw.rs index 374dc6b330..42a6725c8c 100644 --- a/crates/bevy_render/src/render_phase/draw.rs +++ b/crates/bevy_render/src/render_phase/draw.rs @@ -213,8 +213,8 @@ pub trait RenderCommand { /// issuing draw calls, etc.) via the [`TrackedRenderPass`]. fn render<'w>( item: &P, - view: ROQueryItem<'w, Self::ViewQuery>, - entity: Option>, + view: ROQueryItem<'w, '_, Self::ViewQuery>, + entity: Option>, param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult; @@ -246,8 +246,8 @@ macro_rules! render_command_tuple_impl { )] fn render<'w>( _item: &P, - ($($view,)*): ROQueryItem<'w, Self::ViewQuery>, - maybe_entities: Option>, + ($($view,)*): ROQueryItem<'w, '_, Self::ViewQuery>, + maybe_entities: Option>, ($($name,)*): SystemParamItem<'w, '_, Self::Param>, _pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index 6dceaba3c0..35b500059e 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -281,7 +281,7 @@ mod render_entities_world_query_impls { archetype::Archetype, component::{ComponentId, Components, Tick}, entity::Entity, - query::{FilteredAccess, QueryData, ReadOnlyQueryData, WorldQuery}, + query::{FilteredAccess, QueryData, ReadOnlyQueryData, ReleaseStateQueryData, WorldQuery}, storage::{Table, TableRow}, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; @@ -289,22 +289,22 @@ mod render_entities_world_query_impls { /// SAFETY: defers completely to `&RenderEntity` implementation, /// and then only modifies the output safely. unsafe impl WorldQuery for RenderEntity { - type Fetch<'w> = <&'static RenderEntity as WorldQuery>::Fetch<'w>; + type Fetch<'w, 's> = <&'static RenderEntity as WorldQuery>::Fetch<'w, 's>; type State = <&'static RenderEntity as WorldQuery>::State; - fn shrink_fetch<'wlong: 'wshort, 'wshort>( - fetch: Self::Fetch<'wlong>, - ) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w> { + ) -> Self::Fetch<'w, 's> { // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. unsafe { <&RenderEntity as WorldQuery>::init_fetch(world, component_id, last_run, this_run) @@ -314,9 +314,9 @@ mod render_entities_world_query_impls { const IS_DENSE: bool = <&'static RenderEntity as WorldQuery>::IS_DENSE; #[inline] - unsafe fn set_archetype<'w>( - fetch: &mut Self::Fetch<'w>, - component_id: &ComponentId, + unsafe fn set_archetype<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + component_id: &'s ComponentId, archetype: &'w Archetype, table: &'w Table, ) { @@ -327,9 +327,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn set_table<'w>( - fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + &component_id: &'s ComponentId, table: &'w Table, ) { // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. @@ -364,18 +364,20 @@ mod render_entities_world_query_impls { unsafe impl QueryData for RenderEntity { const IS_READ_ONLY: bool = true; type ReadOnly = RenderEntity; - type Item<'w> = Entity; + type Item<'w, 's> = Entity; - fn shrink<'wlong: 'wshort, 'wshort>(item: Entity) -> Entity { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. let component = unsafe { <&RenderEntity as QueryData>::fetch(fetch, entity, table_row) }; @@ -386,25 +388,31 @@ mod render_entities_world_query_impls { // SAFETY: the underlying `Entity` is copied, and no mutable access is provided. unsafe impl ReadOnlyQueryData for RenderEntity {} + impl ReleaseStateQueryData for RenderEntity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } + } + /// SAFETY: defers completely to `&RenderEntity` implementation, /// and then only modifies the output safely. unsafe impl WorldQuery for MainEntity { - type Fetch<'w> = <&'static MainEntity as WorldQuery>::Fetch<'w>; + type Fetch<'w, 's> = <&'static MainEntity as WorldQuery>::Fetch<'w, 's>; type State = <&'static MainEntity as WorldQuery>::State; - fn shrink_fetch<'wlong: 'wshort, 'wshort>( - fetch: Self::Fetch<'wlong>, - ) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w> { + ) -> Self::Fetch<'w, 's> { // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. unsafe { <&MainEntity as WorldQuery>::init_fetch(world, component_id, last_run, this_run) @@ -414,8 +422,8 @@ mod render_entities_world_query_impls { const IS_DENSE: bool = <&'static MainEntity as WorldQuery>::IS_DENSE; #[inline] - unsafe fn set_archetype<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn set_archetype<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, component_id: &ComponentId, archetype: &'w Archetype, table: &'w Table, @@ -427,9 +435,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn set_table<'w>( - fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + &component_id: &'s ComponentId, table: &'w Table, ) { // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. @@ -464,18 +472,20 @@ mod render_entities_world_query_impls { unsafe impl QueryData for MainEntity { const IS_READ_ONLY: bool = true; type ReadOnly = MainEntity; - type Item<'w> = Entity; + type Item<'w, 's> = Entity; - fn shrink<'wlong: 'wshort, 'wshort>(item: Entity) -> Entity { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, + unsafe fn fetch<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. let component = unsafe { <&MainEntity as QueryData>::fetch(fetch, entity, table_row) }; component.id() @@ -484,6 +494,12 @@ mod render_entities_world_query_impls { // SAFETY: the underlying `Entity` is copied, and no mutable access is provided. unsafe impl ReadOnlyQueryData for MainEntity {} + + impl ReleaseStateQueryData for MainEntity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } + } } #[cfg(test)] diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index a3d9ee3eb2..204dd85e24 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -778,7 +778,7 @@ impl RenderCommand

for SetMesh2dViewBindGroup( _item: &P, - (view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, Self::ViewQuery>, + (view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, '_, Self::ViewQuery>, _view: Option<()>, _param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index 8ffb12a582..03de94be1c 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -372,7 +372,7 @@ impl ViewNode for Wireframe2dNode { &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(wireframe_phase) = diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 7602addc0b..761e2c628a 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -908,7 +908,7 @@ impl RenderCommand

for SetSpriteViewBindGroup( _item: &P, - (view_uniform, sprite_view_bind_group): ROQueryItem<'w, Self::ViewQuery>, + (view_uniform, sprite_view_bind_group): ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, _param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, @@ -925,7 +925,7 @@ impl RenderCommand

for SetSpriteTextureBindGrou fn render<'w>( item: &P, - view: ROQueryItem<'w, Self::ViewQuery>, + view: ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, (image_bind_groups, batches): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, @@ -955,7 +955,7 @@ impl RenderCommand

for DrawSpriteBatch { fn render<'w>( item: &P, - view: ROQueryItem<'w, Self::ViewQuery>, + view: ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, (sprite_meta, batches): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index ebdeacccf9..3ad4f4ea6a 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -276,7 +276,7 @@ impl RenderCommand

fn render<'w>( _item: &P, _view: (), - material_handle: Option>, + material_handle: Option>, materials: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { diff --git a/examples/ecs/custom_query_param.rs b/examples/ecs/custom_query_param.rs index 39b3e6f072..8960557ef4 100644 --- a/examples/ecs/custom_query_param.rs +++ b/examples/ecs/custom_query_param.rs @@ -137,7 +137,7 @@ fn print_components_iter_mut( println!("Print components (iter_mut):"); for e in &mut query { // Re-declaring the variable to illustrate the type of the actual iterator item. - let e: CustomQueryItem<'_, _, _> = e; + let e: CustomQueryItem<'_, '_, _, _> = e; println!("Entity: {}", e.entity); println!("A: {:?}", e.a); println!("B: {:?}", e.b); @@ -155,7 +155,7 @@ fn print_components_iter( println!("Print components (iter):"); for e in &query { // Re-declaring the variable to illustrate the type of the actual iterator item. - let e: CustomQueryReadOnlyItem<'_, _, _> = e; + let e: CustomQueryReadOnlyItem<'_, '_, _, _> = e; println!("Entity: {}", e.entity); println!("A: {:?}", e.a); println!("B: {:?}", e.b); diff --git a/examples/shader/custom_phase_item.rs b/examples/shader/custom_phase_item.rs index 1ec61f8126..b363a7c27f 100644 --- a/examples/shader/custom_phase_item.rs +++ b/examples/shader/custom_phase_item.rs @@ -73,8 +73,8 @@ where fn render<'w>( _: &P, - _: ROQueryItem<'w, Self::ViewQuery>, - _: Option>, + _: ROQueryItem<'w, '_, Self::ViewQuery>, + _: Option>, custom_phase_item_buffers: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { diff --git a/examples/shader/custom_render_phase.rs b/examples/shader/custom_render_phase.rs index 6499ca6bd2..8bbf57d73c 100644 --- a/examples/shader/custom_render_phase.rs +++ b/examples/shader/custom_render_phase.rs @@ -588,7 +588,7 @@ impl ViewNode for CustomDrawNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target): QueryItem<'w, Self::ViewQuery>, + (camera, view, target): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { // First, we need to get our phases resource diff --git a/examples/shader/custom_shader_instancing.rs b/examples/shader/custom_shader_instancing.rs index d81c6e53b6..06845b0617 100644 --- a/examples/shader/custom_shader_instancing.rs +++ b/examples/shader/custom_shader_instancing.rs @@ -88,7 +88,7 @@ impl ExtractComponent for InstanceMaterialData { type QueryFilter = (); type Out = Self; - fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { Some(InstanceMaterialData(item.0.clone())) } } diff --git a/release-content/migration-guides/query_items_borrow_from_query_state.md b/release-content/migration-guides/query_items_borrow_from_query_state.md new file mode 100644 index 0000000000..6c5aff0637 --- /dev/null +++ b/release-content/migration-guides/query_items_borrow_from_query_state.md @@ -0,0 +1,65 @@ +--- +title: Query items can borrow from query state +pull_requests: [15396] +--- + +The `QueryData::Item` and `WorldQuery::Fetch` associated types and the `QueryItem` and `ROQueryItem` type aliases now have an additional lifetime parameter corresponding to the `'s` lifetime in `Query`. +Manual implementations of `WorldQuery` and `QueryData` will need to update the method signatures to include the new lifetimes. +Other uses of the types will need to be updated to include a lifetime parameter, although it can usually be passed as `'_`. +In particular, `ROQueryItem` is used when implementing `RenderCommand`. + +Before: + +```rust +// 0.16 +fn render<'w>( + item: &P, + view: ROQueryItem<'w, Self::ViewQuery>, + entity: Option>, + param: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, +) -> RenderCommandResult; + +// 0.17 +fn render<'w>( + item: &P, + view: ROQueryItem<'w, '_, Self::ViewQuery>, + entity: Option>, + param: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, +) -> RenderCommandResult; +``` + +--- + +Methods on `QueryState` that take `&mut self` may now result in conflicting borrows if the query items capture the lifetime of the mutable reference. +This affects `get()`, `iter()`, and others. +To fix the errors, first call `QueryState::update_archetypes()`, and then replace a call `state.foo(world, param)` with `state.query_manual(world).foo_inner(param)`. +Alternately, you may be able to restructure the code to call `state.query(world)` once and then make multiple calls using the `Query`. + +```rust +let mut state: QueryState<_, _> = ...; + +// 0.16 +let d1 = state.get(world, e1); +let d2 = state.get(world, e2); // Error: cannot borrow `state` as mutable more than once at a time + +println!("{d1:?}"); +println!("{d2:?}"); + +// 0.17 +state.update_archetypes(world); +let d1 = state.get_manual(world, e1); +let d2 = state.get_manual(world, e2); +// OR +state.update_archetypes(world); +let d1 = state.query_manual(world).get_inner(e1); +let d2 = state.query_manual(world).get_inner(e2); +// OR +let query = state.query(world); +let d1 = query.get_inner(e1); +let d1 = query.get_inner(e2); + +println!("{d1:?}"); +println!("{d2:?}"); +``` From c8cb7bdf5771c2baab3de82edaf254888a5c27b4 Mon Sep 17 00:00:00 2001 From: Lucas Franca Date: Mon, 16 Jun 2025 18:19:47 -0300 Subject: [PATCH 037/106] Allow passing number of thread for building and testing to CI (#19359) # Objective Fixes #16051 Closes #16145 ## Solution Allow passing `--build-jobs` and `--test-threads` to `ci` i.e. ``` cargo run -p ci -- --build-jobs 4 --test-threads 4 ``` ## Testing running ci locally --------- Co-authored-by: Benjamin Brienen --- tools/ci/Cargo.toml | 1 - tools/ci/src/args.rs | 37 ++++++++ tools/ci/src/ci.rs | 84 ++++++++++--------- tools/ci/src/commands/bench_check.rs | 8 +- tools/ci/src/commands/clippy.rs | 8 +- tools/ci/src/commands/compile.rs | 17 ++-- tools/ci/src/commands/compile_check.rs | 8 +- tools/ci/src/commands/compile_fail.rs | 19 +++-- tools/ci/src/commands/doc.rs | 9 +- tools/ci/src/commands/doc_check.rs | 8 +- tools/ci/src/commands/doc_test.rs | 16 ++-- tools/ci/src/commands/example_check.rs | 8 +- tools/ci/src/commands/format.rs | 4 +- tools/ci/src/commands/integration_test.rs | 15 ++-- .../ci/src/commands/integration_test_check.rs | 12 ++- .../ci/src/commands/integration_test_clean.rs | 4 +- tools/ci/src/commands/lints.rs | 9 +- tools/ci/src/commands/test.rs | 13 ++- tools/ci/src/commands/test_check.rs | 8 +- tools/ci/src/main.rs | 1 + tools/ci/src/prepare.rs | 17 +--- 21 files changed, 181 insertions(+), 125 deletions(-) create mode 100644 tools/ci/src/args.rs diff --git a/tools/ci/Cargo.toml b/tools/ci/Cargo.toml index 65d6b7b1be..d42eb55505 100644 --- a/tools/ci/Cargo.toml +++ b/tools/ci/Cargo.toml @@ -8,7 +8,6 @@ license = "MIT OR Apache-2.0" [dependencies] argh = "0.1" xshell = "0.2" -bitflags = "2.3" [lints] workspace = true diff --git a/tools/ci/src/args.rs b/tools/ci/src/args.rs new file mode 100644 index 0000000000..574d98f192 --- /dev/null +++ b/tools/ci/src/args.rs @@ -0,0 +1,37 @@ +use crate::CI; + +/// Arguments that are available to CI commands. +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct Args { + keep_going: bool, + test_threads: Option, + build_jobs: Option, +} + +impl Args { + #[inline(always)] + pub fn keep_going(&self) -> Option<&'static str> { + self.keep_going.then_some("--no-fail-fast") + } + + #[inline(always)] + pub fn build_jobs(&self) -> Option { + self.build_jobs.map(|jobs| format!("--jobs={jobs}")) + } + + #[inline(always)] + pub fn test_threads(&self) -> Option { + self.test_threads + .map(|threads| format!("--test-threads={threads}")) + } +} + +impl From<&CI> for Args { + fn from(value: &CI) -> Self { + Args { + keep_going: value.keep_going, + test_threads: value.test_threads, + build_jobs: value.build_jobs, + } + } +} diff --git a/tools/ci/src/ci.rs b/tools/ci/src/ci.rs index 349e74a5a0..8b8556d90a 100644 --- a/tools/ci/src/ci.rs +++ b/tools/ci/src/ci.rs @@ -1,6 +1,7 @@ use crate::{ + args::Args, commands, - prepare::{Flag, Prepare, PreparedCommand}, + prepare::{Prepare, PreparedCommand}, }; use argh::FromArgs; @@ -12,7 +13,15 @@ pub struct CI { /// continue running commands even if one fails #[argh(switch)] - keep_going: bool, + pub(crate) keep_going: bool, + + /// parallelism of `cargo test` + #[argh(option)] + pub(crate) test_threads: Option, + + /// number of build jobs + #[argh(option)] + pub(crate) build_jobs: Option, } impl CI { @@ -22,7 +31,6 @@ impl CI { /// This is usually related to differing toolchains and configuration. pub fn run(self) { let sh = xshell::Shell::new().unwrap(); - let prepared_commands = self.prepare(&sh); let mut failures = vec![]; @@ -59,34 +67,30 @@ impl CI { } fn prepare<'a>(&self, sh: &'a xshell::Shell) -> Vec> { - let mut flags = Flag::empty(); - - if self.keep_going { - flags |= Flag::KEEP_GOING; - } - + let args = self.into(); match &self.command { - Some(command) => command.prepare(sh, flags), + Some(command) => command.prepare(sh, args), None => { // Note that we are running the subcommands directly rather than using any aliases let mut cmds = vec![]; - cmds.append(&mut commands::FormatCommand::default().prepare(sh, flags)); - cmds.append(&mut commands::ClippyCommand::default().prepare(sh, flags)); - cmds.append(&mut commands::TestCommand::default().prepare(sh, flags)); - cmds.append(&mut commands::TestCheckCommand::default().prepare(sh, flags)); - cmds.append(&mut commands::IntegrationTestCommand::default().prepare(sh, flags)); + cmds.append(&mut commands::FormatCommand::default().prepare(sh, args)); + cmds.append(&mut commands::ClippyCommand::default().prepare(sh, args)); + cmds.append(&mut commands::TestCommand::default().prepare(sh, args)); + cmds.append(&mut commands::TestCheckCommand::default().prepare(sh, args)); + cmds.append(&mut commands::IntegrationTestCommand::default().prepare(sh, args)); cmds.append( - &mut commands::IntegrationTestCheckCommand::default().prepare(sh, flags), + &mut commands::IntegrationTestCheckCommand::default().prepare(sh, args), ); cmds.append( - &mut commands::IntegrationTestCleanCommand::default().prepare(sh, flags), + &mut commands::IntegrationTestCleanCommand::default().prepare(sh, args), ); - cmds.append(&mut commands::DocCheckCommand::default().prepare(sh, flags)); - cmds.append(&mut commands::DocTestCommand::default().prepare(sh, flags)); - cmds.append(&mut commands::CompileCheckCommand::default().prepare(sh, flags)); - cmds.append(&mut commands::CompileFailCommand::default().prepare(sh, flags)); - cmds.append(&mut commands::BenchCheckCommand::default().prepare(sh, flags)); - cmds.append(&mut commands::ExampleCheckCommand::default().prepare(sh, flags)); + cmds.append(&mut commands::DocCheckCommand::default().prepare(sh, args)); + cmds.append(&mut commands::DocTestCommand::default().prepare(sh, args)); + cmds.append(&mut commands::CompileCheckCommand::default().prepare(sh, args)); + cmds.append(&mut commands::CompileFailCommand::default().prepare(sh, args)); + cmds.append(&mut commands::BenchCheckCommand::default().prepare(sh, args)); + cmds.append(&mut commands::ExampleCheckCommand::default().prepare(sh, args)); + cmds } } @@ -118,25 +122,25 @@ enum Commands { } impl Prepare for Commands { - fn prepare<'a>(&self, sh: &'a xshell::Shell, flags: Flag) -> Vec> { + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { match self { - Commands::Lints(subcommand) => subcommand.prepare(sh, flags), - Commands::Doc(subcommand) => subcommand.prepare(sh, flags), - Commands::Compile(subcommand) => subcommand.prepare(sh, flags), + Commands::Lints(subcommand) => subcommand.prepare(sh, args), + Commands::Doc(subcommand) => subcommand.prepare(sh, args), + Commands::Compile(subcommand) => subcommand.prepare(sh, args), - Commands::Format(subcommand) => subcommand.prepare(sh, flags), - Commands::Clippy(subcommand) => subcommand.prepare(sh, flags), - Commands::Test(subcommand) => subcommand.prepare(sh, flags), - Commands::TestCheck(subcommand) => subcommand.prepare(sh, flags), - Commands::IntegrationTest(subcommand) => subcommand.prepare(sh, flags), - Commands::IntegrationTestCheck(subcommand) => subcommand.prepare(sh, flags), - Commands::IntegrationTestClean(subcommand) => subcommand.prepare(sh, flags), - Commands::DocCheck(subcommand) => subcommand.prepare(sh, flags), - Commands::DocTest(subcommand) => subcommand.prepare(sh, flags), - Commands::CompileCheck(subcommand) => subcommand.prepare(sh, flags), - Commands::CompileFail(subcommand) => subcommand.prepare(sh, flags), - Commands::BenchCheck(subcommand) => subcommand.prepare(sh, flags), - Commands::ExampleCheck(subcommand) => subcommand.prepare(sh, flags), + Commands::Format(subcommand) => subcommand.prepare(sh, args), + Commands::Clippy(subcommand) => subcommand.prepare(sh, args), + Commands::Test(subcommand) => subcommand.prepare(sh, args), + Commands::TestCheck(subcommand) => subcommand.prepare(sh, args), + Commands::IntegrationTest(subcommand) => subcommand.prepare(sh, args), + Commands::IntegrationTestCheck(subcommand) => subcommand.prepare(sh, args), + Commands::IntegrationTestClean(subcommand) => subcommand.prepare(sh, args), + Commands::DocCheck(subcommand) => subcommand.prepare(sh, args), + Commands::DocTest(subcommand) => subcommand.prepare(sh, args), + Commands::CompileCheck(subcommand) => subcommand.prepare(sh, args), + Commands::CompileFail(subcommand) => subcommand.prepare(sh, args), + Commands::BenchCheck(subcommand) => subcommand.prepare(sh, args), + Commands::ExampleCheck(subcommand) => subcommand.prepare(sh, args), } } } diff --git a/tools/ci/src/commands/bench_check.rs b/tools/ci/src/commands/bench_check.rs index 9e72ab0a64..748f10a730 100644 --- a/tools/ci/src/commands/bench_check.rs +++ b/tools/ci/src/commands/bench_check.rs @@ -1,4 +1,4 @@ -use crate::{Flag, Prepare, PreparedCommand}; +use crate::{args::Args, Prepare, PreparedCommand}; use argh::FromArgs; use xshell::cmd; @@ -8,11 +8,13 @@ use xshell::cmd; pub struct BenchCheckCommand {} impl Prepare for BenchCheckCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, _flags: Flag) -> Vec> { + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { + let jobs = args.build_jobs(); + vec![PreparedCommand::new::( cmd!( sh, - "cargo check --benches --target-dir ../target --manifest-path ./benches/Cargo.toml" + "cargo check --benches {jobs...} --target-dir ../target --manifest-path ./benches/Cargo.toml" ), "Failed to check the benches.", )] diff --git a/tools/ci/src/commands/clippy.rs b/tools/ci/src/commands/clippy.rs index 5e097c05a1..7d3d9afb29 100644 --- a/tools/ci/src/commands/clippy.rs +++ b/tools/ci/src/commands/clippy.rs @@ -1,4 +1,4 @@ -use crate::{Flag, Prepare, PreparedCommand}; +use crate::{args::Args, Prepare, PreparedCommand}; use argh::FromArgs; use xshell::cmd; @@ -8,11 +8,13 @@ use xshell::cmd; pub struct ClippyCommand {} impl Prepare for ClippyCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, _flags: Flag) -> Vec> { + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { + let jobs = args.build_jobs(); + vec![PreparedCommand::new::( cmd!( sh, - "cargo clippy --workspace --all-targets --all-features -- -Dwarnings" + "cargo clippy --workspace --all-targets --all-features {jobs...} -- -Dwarnings" ), "Please fix clippy errors in output above.", )] diff --git a/tools/ci/src/commands/compile.rs b/tools/ci/src/commands/compile.rs index f445aaca57..622b5b27f0 100644 --- a/tools/ci/src/commands/compile.rs +++ b/tools/ci/src/commands/compile.rs @@ -1,9 +1,10 @@ use crate::{ + args::Args, commands::{ BenchCheckCommand, CompileCheckCommand, CompileFailCommand, ExampleCheckCommand, IntegrationTestCheckCommand, TestCheckCommand, }, - Flag, Prepare, PreparedCommand, + Prepare, PreparedCommand, }; use argh::FromArgs; @@ -13,14 +14,14 @@ use argh::FromArgs; pub struct CompileCommand {} impl Prepare for CompileCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, flags: Flag) -> Vec> { + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { let mut commands = vec![]; - commands.append(&mut CompileFailCommand::default().prepare(sh, flags)); - commands.append(&mut BenchCheckCommand::default().prepare(sh, flags)); - commands.append(&mut ExampleCheckCommand::default().prepare(sh, flags)); - commands.append(&mut CompileCheckCommand::default().prepare(sh, flags)); - commands.append(&mut TestCheckCommand::default().prepare(sh, flags)); - commands.append(&mut IntegrationTestCheckCommand::default().prepare(sh, flags)); + commands.append(&mut CompileFailCommand::default().prepare(sh, args)); + commands.append(&mut BenchCheckCommand::default().prepare(sh, args)); + commands.append(&mut ExampleCheckCommand::default().prepare(sh, args)); + commands.append(&mut CompileCheckCommand::default().prepare(sh, args)); + commands.append(&mut TestCheckCommand::default().prepare(sh, args)); + commands.append(&mut IntegrationTestCheckCommand::default().prepare(sh, args)); commands } } diff --git a/tools/ci/src/commands/compile_check.rs b/tools/ci/src/commands/compile_check.rs index 62d8a8da75..e3628dc381 100644 --- a/tools/ci/src/commands/compile_check.rs +++ b/tools/ci/src/commands/compile_check.rs @@ -1,4 +1,4 @@ -use crate::{Flag, Prepare, PreparedCommand}; +use crate::{args::Args, Prepare, PreparedCommand}; use argh::FromArgs; use xshell::cmd; @@ -8,9 +8,11 @@ use xshell::cmd; pub struct CompileCheckCommand {} impl Prepare for CompileCheckCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, _flags: Flag) -> Vec> { + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { + let jobs = args.build_jobs(); + vec![PreparedCommand::new::( - cmd!(sh, "cargo check --workspace"), + cmd!(sh, "cargo check --workspace {jobs...}"), "Please fix compiler errors in output above.", )] } diff --git a/tools/ci/src/commands/compile_fail.rs b/tools/ci/src/commands/compile_fail.rs index 65a91e6d2f..e2c6e62d94 100644 --- a/tools/ci/src/commands/compile_fail.rs +++ b/tools/ci/src/commands/compile_fail.rs @@ -1,4 +1,4 @@ -use crate::{Flag, Prepare, PreparedCommand}; +use crate::{args::Args, Prepare, PreparedCommand}; use argh::FromArgs; use xshell::cmd; @@ -8,11 +8,12 @@ use xshell::cmd; pub struct CompileFailCommand {} impl Prepare for CompileFailCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, flags: Flag) -> Vec> { - let no_fail_fast = flags - .contains(Flag::KEEP_GOING) - .then_some("--no-fail-fast") - .unwrap_or_default(); + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { + let no_fail_fast = args.keep_going(); + let jobs = args.build_jobs(); + let test_threads = args.test_threads(); + let jobs_ref = jobs.as_ref(); + let test_threads_ref = test_threads.as_ref(); let mut commands = vec![]; @@ -21,7 +22,7 @@ impl Prepare for CompileFailCommand { // - See crates/bevy_macros_compile_fail_tests/README.md commands.push( PreparedCommand::new::( - cmd!(sh, "cargo test --target-dir ../../../target {no_fail_fast}"), + cmd!(sh, "cargo test --target-dir ../../../target {no_fail_fast...} {jobs_ref...} -- {test_threads_ref...}"), "Compiler errors of the macros compile fail tests seem to be different than expected! Check locally and compare rust versions.", ) .with_subdir("crates/bevy_derive/compile_fail"), @@ -32,7 +33,7 @@ impl Prepare for CompileFailCommand { // - See crates/bevy_ecs_compile_fail_tests/README.md commands.push( PreparedCommand::new::( - cmd!(sh, "cargo test --target-dir ../../../target {no_fail_fast}"), + cmd!(sh, "cargo test --target-dir ../../../target {no_fail_fast...} {jobs_ref...} -- {test_threads_ref...}"), "Compiler errors of the ECS compile fail tests seem to be different than expected! Check locally and compare rust versions.", ) .with_subdir("crates/bevy_ecs/compile_fail"), @@ -43,7 +44,7 @@ impl Prepare for CompileFailCommand { // - See crates/bevy_reflect_compile_fail_tests/README.md commands.push( PreparedCommand::new::( - cmd!(sh, "cargo test --target-dir ../../../target {no_fail_fast}"), + cmd!(sh, "cargo test --target-dir ../../../target {no_fail_fast...} {jobs...} -- {test_threads...}"), "Compiler errors of the Reflect compile fail tests seem to be different than expected! Check locally and compare rust versions.", ) .with_subdir("crates/bevy_reflect/compile_fail"), diff --git a/tools/ci/src/commands/doc.rs b/tools/ci/src/commands/doc.rs index fb8074ca8c..90fbbd30a5 100644 --- a/tools/ci/src/commands/doc.rs +++ b/tools/ci/src/commands/doc.rs @@ -1,6 +1,7 @@ use crate::{ + args::Args, commands::{DocCheckCommand, DocTestCommand}, - Flag, Prepare, PreparedCommand, + Prepare, PreparedCommand, }; use argh::FromArgs; @@ -10,10 +11,10 @@ use argh::FromArgs; pub struct DocCommand {} impl Prepare for DocCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, flags: Flag) -> Vec> { + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { let mut commands = vec![]; - commands.append(&mut DocTestCommand::default().prepare(sh, flags)); - commands.append(&mut DocCheckCommand::default().prepare(sh, flags)); + commands.append(&mut DocTestCommand::default().prepare(sh, args)); + commands.append(&mut DocCheckCommand::default().prepare(sh, args)); commands } } diff --git a/tools/ci/src/commands/doc_check.rs b/tools/ci/src/commands/doc_check.rs index ef5fc502df..75bf0cddf1 100644 --- a/tools/ci/src/commands/doc_check.rs +++ b/tools/ci/src/commands/doc_check.rs @@ -1,4 +1,4 @@ -use crate::{Flag, Prepare, PreparedCommand}; +use crate::{args::Args, Prepare, PreparedCommand}; use argh::FromArgs; use xshell::cmd; @@ -8,11 +8,13 @@ use xshell::cmd; pub struct DocCheckCommand {} impl Prepare for DocCheckCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, _flags: Flag) -> Vec> { + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { + let jobs = args.build_jobs(); + vec![PreparedCommand::new::( cmd!( sh, - "cargo doc --workspace --all-features --no-deps --document-private-items --keep-going" + "cargo doc --workspace --all-features --no-deps --document-private-items {jobs...} --keep-going" ), "Please fix doc warnings in output above.", ) diff --git a/tools/ci/src/commands/doc_test.rs b/tools/ci/src/commands/doc_test.rs index c1ce22c3e6..67d1d21720 100644 --- a/tools/ci/src/commands/doc_test.rs +++ b/tools/ci/src/commands/doc_test.rs @@ -1,4 +1,4 @@ -use crate::{Flag, Prepare, PreparedCommand}; +use crate::{args::Args, Prepare, PreparedCommand}; use argh::FromArgs; use xshell::cmd; @@ -8,14 +8,16 @@ use xshell::cmd; pub struct DocTestCommand {} impl Prepare for DocTestCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, flags: Flag) -> Vec> { - let no_fail_fast = flags - .contains(Flag::KEEP_GOING) - .then_some("--no-fail-fast") - .unwrap_or_default(); + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { + let no_fail_fast = args.keep_going(); + let jobs = args.build_jobs(); + let test_threads = args.test_threads(); vec![PreparedCommand::new::( - cmd!(sh, "cargo test --workspace --doc {no_fail_fast}"), + cmd!( + sh, + "cargo test --workspace --doc {no_fail_fast...} {jobs...} -- {test_threads...}" + ), "Please fix failing doc tests in output above.", )] } diff --git a/tools/ci/src/commands/example_check.rs b/tools/ci/src/commands/example_check.rs index d3d3f5ddf2..6533dbac0e 100644 --- a/tools/ci/src/commands/example_check.rs +++ b/tools/ci/src/commands/example_check.rs @@ -1,4 +1,4 @@ -use crate::{Flag, Prepare, PreparedCommand}; +use crate::{args::Args, Prepare, PreparedCommand}; use argh::FromArgs; use xshell::cmd; @@ -8,9 +8,11 @@ use xshell::cmd; pub struct ExampleCheckCommand {} impl Prepare for ExampleCheckCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, _flags: Flag) -> Vec> { + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { + let jobs = args.build_jobs(); + vec![PreparedCommand::new::( - cmd!(sh, "cargo check --workspace --examples"), + cmd!(sh, "cargo check --workspace --examples {jobs...}"), "Please fix compiler errors for examples in output above.", )] } diff --git a/tools/ci/src/commands/format.rs b/tools/ci/src/commands/format.rs index f5aacc5301..a6ae00cf1f 100644 --- a/tools/ci/src/commands/format.rs +++ b/tools/ci/src/commands/format.rs @@ -1,4 +1,4 @@ -use crate::{Flag, Prepare, PreparedCommand}; +use crate::{args::Args, Prepare, PreparedCommand}; use argh::FromArgs; use xshell::cmd; @@ -8,7 +8,7 @@ use xshell::cmd; pub struct FormatCommand {} impl Prepare for FormatCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, _flags: Flag) -> Vec> { + fn prepare<'a>(&self, sh: &'a xshell::Shell, _args: Args) -> Vec> { vec![PreparedCommand::new::( cmd!(sh, "cargo fmt --all -- --check"), "Please run 'cargo fmt --all' to format your code.", diff --git a/tools/ci/src/commands/integration_test.rs b/tools/ci/src/commands/integration_test.rs index b86a027a56..35fefacd24 100644 --- a/tools/ci/src/commands/integration_test.rs +++ b/tools/ci/src/commands/integration_test.rs @@ -1,4 +1,4 @@ -use crate::{commands::get_integration_tests, Flag, Prepare, PreparedCommand}; +use crate::{args::Args, commands::get_integration_tests, Prepare, PreparedCommand}; use argh::FromArgs; use xshell::cmd; @@ -8,11 +8,12 @@ use xshell::cmd; pub struct IntegrationTestCommand {} impl Prepare for IntegrationTestCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, flags: Flag) -> Vec> { - let no_fail_fast = flags - .contains(Flag::KEEP_GOING) - .then_some("--no-fail-fast") - .unwrap_or_default(); + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { + let no_fail_fast = args.keep_going(); + let jobs = args.build_jobs(); + let test_threads = args.test_threads(); + let jobs_ref = jobs.as_ref(); + let test_threads_ref = test_threads.as_ref(); get_integration_tests(sh) .into_iter() @@ -20,7 +21,7 @@ impl Prepare for IntegrationTestCommand { PreparedCommand::new::( cmd!( sh, - "cargo test --manifest-path {path}/Cargo.toml --tests {no_fail_fast}" + "cargo test --manifest-path {path}/Cargo.toml --tests {no_fail_fast...} {jobs_ref...} -- {test_threads_ref...}" ), "Please fix failing integration tests in output above.", ) diff --git a/tools/ci/src/commands/integration_test_check.rs b/tools/ci/src/commands/integration_test_check.rs index d10808a746..af47b9bffa 100644 --- a/tools/ci/src/commands/integration_test_check.rs +++ b/tools/ci/src/commands/integration_test_check.rs @@ -1,6 +1,6 @@ use std::path::Path; -use crate::{Flag, Prepare, PreparedCommand}; +use crate::{args::Args, Prepare, PreparedCommand}; use argh::FromArgs; use xshell::cmd; @@ -21,12 +21,18 @@ pub fn get_integration_tests(sh: &xshell::Shell) -> Vec { pub struct IntegrationTestCheckCommand {} impl Prepare for IntegrationTestCheckCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, _flags: Flag) -> Vec> { + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { + let jobs = args.build_jobs(); + let jobs_ref = jobs.as_ref(); + get_integration_tests(sh) .into_iter() .map(|path| { PreparedCommand::new::( - cmd!(sh, "cargo check --manifest-path {path}/Cargo.toml --tests"), + cmd!( + sh, + "cargo check --manifest-path {path}/Cargo.toml --tests {jobs_ref...}" + ), "Please fix compiler errors for tests in output above.", ) }) diff --git a/tools/ci/src/commands/integration_test_clean.rs b/tools/ci/src/commands/integration_test_clean.rs index 9ad7a44f0b..5ac13bf6ff 100644 --- a/tools/ci/src/commands/integration_test_clean.rs +++ b/tools/ci/src/commands/integration_test_clean.rs @@ -1,4 +1,4 @@ -use crate::{Flag, Prepare, PreparedCommand}; +use crate::{args::Args, Prepare, PreparedCommand}; use argh::FromArgs; use xshell::cmd; @@ -10,7 +10,7 @@ use super::get_integration_tests; pub struct IntegrationTestCleanCommand {} impl Prepare for IntegrationTestCleanCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, _flags: Flag) -> Vec> { + fn prepare<'a>(&self, sh: &'a xshell::Shell, _args: Args) -> Vec> { get_integration_tests(sh) .into_iter() .map(|path| { diff --git a/tools/ci/src/commands/lints.rs b/tools/ci/src/commands/lints.rs index befdaf5fc5..f21bb1b8ef 100644 --- a/tools/ci/src/commands/lints.rs +++ b/tools/ci/src/commands/lints.rs @@ -1,6 +1,7 @@ use crate::{ + args::Args, commands::{ClippyCommand, FormatCommand}, - Flag, Prepare, PreparedCommand, + Prepare, PreparedCommand, }; use argh::FromArgs; @@ -10,10 +11,10 @@ use argh::FromArgs; pub struct LintsCommand {} impl Prepare for LintsCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, flags: Flag) -> Vec> { + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { let mut commands = vec![]; - commands.append(&mut FormatCommand::default().prepare(sh, flags)); - commands.append(&mut ClippyCommand::default().prepare(sh, flags)); + commands.append(&mut FormatCommand::default().prepare(sh, args)); + commands.append(&mut ClippyCommand::default().prepare(sh, args)); commands } } diff --git a/tools/ci/src/commands/test.rs b/tools/ci/src/commands/test.rs index bdb4b663d1..a904e59c46 100644 --- a/tools/ci/src/commands/test.rs +++ b/tools/ci/src/commands/test.rs @@ -1,4 +1,4 @@ -use crate::{Flag, Prepare, PreparedCommand}; +use crate::{args::Args, Prepare, PreparedCommand}; use argh::FromArgs; use xshell::cmd; @@ -8,18 +8,17 @@ use xshell::cmd; pub struct TestCommand {} impl Prepare for TestCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, flags: Flag) -> Vec> { - let no_fail_fast = flags - .contains(Flag::KEEP_GOING) - .then_some("--no-fail-fast") - .unwrap_or_default(); + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { + let no_fail_fast = args.keep_going(); + let jobs = args.build_jobs(); + let test_threads = args.test_threads(); vec![PreparedCommand::new::( cmd!( sh, // `--benches` runs each benchmark once in order to verify that they behave // correctly and do not panic. - "cargo test --workspace --lib --bins --tests --benches {no_fail_fast}" + "cargo test --workspace --lib --bins --tests --benches {no_fail_fast...} {jobs...} -- {test_threads...}" ), "Please fix failing tests in output above.", )] diff --git a/tools/ci/src/commands/test_check.rs b/tools/ci/src/commands/test_check.rs index 8671b4f6cf..9f62b15ad1 100644 --- a/tools/ci/src/commands/test_check.rs +++ b/tools/ci/src/commands/test_check.rs @@ -1,4 +1,4 @@ -use crate::{Flag, Prepare, PreparedCommand}; +use crate::{args::Args, Prepare, PreparedCommand}; use argh::FromArgs; use xshell::cmd; @@ -8,9 +8,11 @@ use xshell::cmd; pub struct TestCheckCommand {} impl Prepare for TestCheckCommand { - fn prepare<'a>(&self, sh: &'a xshell::Shell, _flags: Flag) -> Vec> { + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec> { + let jobs = args.build_jobs(); + vec![PreparedCommand::new::( - cmd!(sh, "cargo check --workspace --tests"), + cmd!(sh, "cargo check --workspace --tests {jobs...}"), "Please fix compiler errors for tests in output above.", )] } diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 40893a528d..7758fc9202 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -1,5 +1,6 @@ //! CI script used for Bevy. +mod args; mod ci; mod commands; mod prepare; diff --git a/tools/ci/src/prepare.rs b/tools/ci/src/prepare.rs index 923bc33f80..b3a3d8f568 100644 --- a/tools/ci/src/prepare.rs +++ b/tools/ci/src/prepare.rs @@ -1,4 +1,4 @@ -use bitflags::bitflags; +use crate::args::Args; /// Trait for preparing a subcommand to be run. pub trait Prepare { @@ -7,7 +7,7 @@ pub trait Prepare { /// # Example /// /// ``` - /// # use crate::{Flag, Prepare, PreparedCommand}; + /// # use crate::{args::Args, Prepare, PreparedCommand}; /// # use argh::FromArgs; /// # use xshell::Shell; /// # @@ -16,7 +16,7 @@ pub trait Prepare { /// struct CheckCommand {} /// /// impl Prepare for CheckCommand { - /// fn prepare<'a>(&self, sh: &'a Shell, flags: Flag) -> Vec> { + /// fn prepare<'a>(&self, sh: &'a Shell, args: Args) -> Vec> { /// vec![PreparedCommand::new::( /// cmd!(sh, "cargo check --workspace"), /// "Please fix linter errors", @@ -24,16 +24,7 @@ pub trait Prepare { /// } /// } /// ``` - fn prepare<'a>(&self, sh: &'a xshell::Shell, flags: Flag) -> Vec>; -} - -bitflags! { - /// Flags that modify how commands are run. - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub struct Flag: u32 { - /// Forces certain checks to continue running even if they hit an error. - const KEEP_GOING = 1 << 0; - } + fn prepare<'a>(&self, sh: &'a xshell::Shell, args: Args) -> Vec>; } /// A command with associated metadata, created from a command that implements [`Prepare`]. From 6db71367d4d2d968b47e9382b85c495596b21f0f Mon Sep 17 00:00:00 2001 From: Austreelis <56743515+Austreelis@users.noreply.github.com> Date: Mon, 16 Jun 2025 23:24:01 +0200 Subject: [PATCH 038/106] allow access to the source error of `AssetLoaderError` and downcasting (#19471) # Objective I have a custom asset loader, and need access to the error it reports when failing to load (e.g. through `AssetLoadFailedEvent { error: AssetLoadError::AssetLoaderError(loader_error), .. }`). However `AssetLoaderError` doesn't expose its `::source()` (i.e. its `error` field. It only formats it when `Display`ed. *I haven't searched for issues about it.* ## Solution - Annotate `AssetLoaderError`'s `error` field with `#[source]`. - Don't include the error when `AssetLoaderError` is `Display`ed (when one prints an error's source stack like a backtrace, it would now be dupplicated). - (optional, included as a separated commit) Add a getter for the `&dyn Error` stored in the `error` field (whithin an `Arc`). This is more ergonomic than using `Error::source()` because it casts an `&Arc` into an `&dyn Error`, meaning one has to downcast it twice to get the original error from the loader, including once where you have to specify the correct type of the *private* `error` field. So downcasting from `Error::source()` effectively rely on the internal implementation of `AssetLoaderError`. The getter instead return the trait object directly, which mean it will directly downcast to the expected loader error type. I didn't included a test that checks that double-downcasting `::source()` doesn't break user code that would rely on the private field's type. ## Testing - Downcasting the trait objects for both `source()` and the `error()` getter work as described above. - `cargo test -p bevy_asset --all-features` pass without errors. --------- Co-authored-by: austreelis --- crates/bevy_asset/src/server/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) 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`. From 3b25b38bdc26b255df9fa2827de9e92b053edad4 Mon Sep 17 00:00:00 2001 From: theotherphil Date: Mon, 16 Jun 2025 22:26:24 +0100 Subject: [PATCH 039/106] deny(missing_docs) for bevy_reflect (#19481) ## Objective Deny missing docs in bevy_reflect, towards https://github.com/bevyengine/bevy/issues/3492. ## Solution Add the missing docs! ## Testing N/A --- crates/bevy_reflect/src/array.rs | 1 + crates/bevy_reflect/src/attributes.rs | 5 + crates/bevy_reflect/src/enums/dynamic_enum.rs | 3 + crates/bevy_reflect/src/enums/enum_trait.rs | 6 ++ crates/bevy_reflect/src/enums/variants.rs | 3 + crates/bevy_reflect/src/error.rs | 15 ++- crates/bevy_reflect/src/fields.rs | 3 + crates/bevy_reflect/src/func/args/arg.rs | 3 + crates/bevy_reflect/src/func/args/error.rs | 6 ++ crates/bevy_reflect/src/func/error.rs | 12 ++- crates/bevy_reflect/src/func/info.rs | 6 ++ crates/bevy_reflect/src/func/mod.rs | 2 +- crates/bevy_reflect/src/func/registry.rs | 1 + crates/bevy_reflect/src/kind.rs | 92 +++++++++++++++++++ crates/bevy_reflect/src/lib.rs | 1 - crates/bevy_reflect/src/map.rs | 2 + crates/bevy_reflect/src/reflect.rs | 19 +++- .../src/serde/de/deserialize_with_registry.rs | 3 + .../src/serde/de/registrations.rs | 1 + crates/bevy_reflect/src/serde/mod.rs | 2 + .../src/serde/ser/serializable.rs | 2 + .../src/serde/ser/serialize_with_registry.rs | 3 + crates/bevy_reflect/src/std_traits.rs | 3 + crates/bevy_reflect/src/struct_trait.rs | 2 + crates/bevy_reflect/src/tuple.rs | 1 + crates/bevy_reflect/src/tuple_struct.rs | 1 + crates/bevy_reflect/src/type_info.rs | 31 ++++++- crates/bevy_reflect/src/type_registry.rs | 6 ++ crates/bevy_reflect/src/utility.rs | 3 +- 29 files changed, 229 insertions(+), 9 deletions(-) diff --git a/crates/bevy_reflect/src/array.rs b/crates/bevy_reflect/src/array.rs index 55f62b34c8..df9580b820 100644 --- a/crates/bevy_reflect/src/array.rs +++ b/crates/bevy_reflect/src/array.rs @@ -167,6 +167,7 @@ pub struct DynamicArray { } impl DynamicArray { + /// Creates a new [`DynamicArray`]. #[inline] pub fn new(values: Box<[Box]>) -> Self { Self { diff --git a/crates/bevy_reflect/src/attributes.rs b/crates/bevy_reflect/src/attributes.rs index 728102c4b0..4d8154fad3 100644 --- a/crates/bevy_reflect/src/attributes.rs +++ b/crates/bevy_reflect/src/attributes.rs @@ -1,3 +1,5 @@ +//! Types and functions for creating, manipulating and querying [`CustomAttributes`]. + use crate::Reflect; use alloc::boxed::Box; use bevy_utils::TypeIdMap; @@ -98,16 +100,19 @@ struct CustomAttribute { } impl CustomAttribute { + /// Creates a new [`CustomAttribute`] containing `value`. pub fn new(value: T) -> Self { Self { value: Box::new(value), } } + /// Returns a reference to the attribute's value if it is of type `T`, or [`None`] if not. pub fn value(&self) -> Option<&T> { self.value.downcast_ref() } + /// Returns a reference to the attribute's value as a [`Reflect`] trait object. pub fn reflect_value(&self) -> &dyn Reflect { &*self.value } diff --git a/crates/bevy_reflect/src/enums/dynamic_enum.rs b/crates/bevy_reflect/src/enums/dynamic_enum.rs index 3f0b275519..2835306b22 100644 --- a/crates/bevy_reflect/src/enums/dynamic_enum.rs +++ b/crates/bevy_reflect/src/enums/dynamic_enum.rs @@ -13,9 +13,12 @@ use derive_more::derive::From; /// A dynamic representation of an enum variant. #[derive(Debug, Default, From)] pub enum DynamicVariant { + /// A unit variant. #[default] Unit, + /// A tuple variant. Tuple(DynamicTuple), + /// A struct variant. Struct(DynamicStruct), } diff --git a/crates/bevy_reflect/src/enums/enum_trait.rs b/crates/bevy_reflect/src/enums/enum_trait.rs index 126c407f23..32e4b96124 100644 --- a/crates/bevy_reflect/src/enums/enum_trait.rs +++ b/crates/bevy_reflect/src/enums/enum_trait.rs @@ -263,6 +263,7 @@ pub struct VariantFieldIter<'a> { } impl<'a> VariantFieldIter<'a> { + /// Creates a new [`VariantFieldIter`]. pub fn new(container: &'a dyn Enum) -> Self { Self { container, @@ -295,12 +296,16 @@ impl<'a> Iterator for VariantFieldIter<'a> { impl<'a> ExactSizeIterator for VariantFieldIter<'a> {} +/// A field in the current enum variant. pub enum VariantField<'a> { + /// The name and value of a field in a struct variant. Struct(&'a str, &'a dyn PartialReflect), + /// The value of a field in a tuple variant. Tuple(&'a dyn PartialReflect), } impl<'a> VariantField<'a> { + /// Returns the name of a struct variant field, or [`None`] for a tuple variant field. pub fn name(&self) -> Option<&'a str> { if let Self::Struct(name, ..) = self { Some(*name) @@ -309,6 +314,7 @@ impl<'a> VariantField<'a> { } } + /// Gets a reference to the value of this field. pub fn value(&self) -> &'a dyn PartialReflect { match *self { Self::Struct(_, value) | Self::Tuple(value) => value, diff --git a/crates/bevy_reflect/src/enums/variants.rs b/crates/bevy_reflect/src/enums/variants.rs index 55ccb8efb1..d4fcc2845a 100644 --- a/crates/bevy_reflect/src/enums/variants.rs +++ b/crates/bevy_reflect/src/enums/variants.rs @@ -47,7 +47,9 @@ pub enum VariantInfoError { /// [type]: VariantType #[error("variant type mismatch: expected {expected:?}, received {received:?}")] TypeMismatch { + /// Expected variant type. expected: VariantType, + /// Received variant type. received: VariantType, }, } @@ -84,6 +86,7 @@ pub enum VariantInfo { } impl VariantInfo { + /// The name of the enum variant. pub fn name(&self) -> &'static str { match self { Self::Struct(info) => info.name(), diff --git a/crates/bevy_reflect/src/error.rs b/crates/bevy_reflect/src/error.rs index a13b55cdc0..d8bb8a9e14 100644 --- a/crates/bevy_reflect/src/error.rs +++ b/crates/bevy_reflect/src/error.rs @@ -11,14 +11,20 @@ pub enum ReflectCloneError { /// /// [`PartialReflect::reflect_clone`]: crate::PartialReflect::reflect_clone #[error("`PartialReflect::reflect_clone` not implemented for `{type_path}`")] - NotImplemented { type_path: Cow<'static, str> }, + NotImplemented { + /// The fully qualified path of the type that [`PartialReflect::reflect_clone`](crate::PartialReflect::reflect_clone) is not implemented for. + type_path: Cow<'static, str>, + }, /// The type cannot be cloned via [`PartialReflect::reflect_clone`]. /// /// This type should be returned when a type is intentionally opting out of reflection cloning. /// /// [`PartialReflect::reflect_clone`]: crate::PartialReflect::reflect_clone #[error("`{type_path}` cannot be made cloneable for `PartialReflect::reflect_clone`")] - NotCloneable { type_path: Cow<'static, str> }, + NotCloneable { + /// The fully qualified path of the type that cannot be cloned via [`PartialReflect::reflect_clone`](crate::PartialReflect::reflect_clone). + type_path: Cow<'static, str>, + }, /// The field cannot be cloned via [`PartialReflect::reflect_clone`]. /// /// When [deriving `Reflect`], this usually means that a field marked with `#[reflect(ignore)]` @@ -33,8 +39,11 @@ pub enum ReflectCloneError { full_path(.field, .variant.as_deref(), .container_type_path) )] FieldNotCloneable { + /// Struct field or enum variant field which cannot be cloned. field: FieldId, + /// Variant this field is part of if the container is an enum, otherwise [`None`]. variant: Option>, + /// Fully qualified path of the type containing this field. container_type_path: Cow<'static, str>, }, /// Could not downcast to the expected type. @@ -44,7 +53,9 @@ pub enum ReflectCloneError { /// [`Reflect`]: crate::Reflect #[error("expected downcast to `{expected}`, but received `{received}`")] FailedDowncast { + /// The fully qualified path of the type that was expected. expected: Cow<'static, str>, + /// The fully qualified path of the type that was received. received: Cow<'static, str>, }, } diff --git a/crates/bevy_reflect/src/fields.rs b/crates/bevy_reflect/src/fields.rs index 21d4ccd98a..53223835b3 100644 --- a/crates/bevy_reflect/src/fields.rs +++ b/crates/bevy_reflect/src/fields.rs @@ -82,6 +82,7 @@ pub struct UnnamedField { } impl UnnamedField { + /// Create a new [`UnnamedField`]. pub fn new(index: usize) -> Self { Self { index, @@ -135,7 +136,9 @@ impl UnnamedField { /// A representation of a field's accessor. #[derive(Clone, Debug, PartialEq, Eq)] pub enum FieldId { + /// Access a field by name. Named(Cow<'static, str>), + /// Access a field by index. Unnamed(usize), } diff --git a/crates/bevy_reflect/src/func/args/arg.rs b/crates/bevy_reflect/src/func/args/arg.rs index 8ca03aafd3..1c157a6b2f 100644 --- a/crates/bevy_reflect/src/func/args/arg.rs +++ b/crates/bevy_reflect/src/func/args/arg.rs @@ -196,8 +196,11 @@ impl<'a> Arg<'a> { /// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug)] pub enum ArgValue<'a> { + /// An owned argument. Owned(Box), + /// An immutable reference argument. Ref(&'a dyn PartialReflect), + /// A mutable reference argument. Mut(&'a mut dyn PartialReflect), } diff --git a/crates/bevy_reflect/src/func/args/error.rs b/crates/bevy_reflect/src/func/args/error.rs index bd32bd5e5a..20b6cd6220 100644 --- a/crates/bevy_reflect/src/func/args/error.rs +++ b/crates/bevy_reflect/src/func/args/error.rs @@ -12,15 +12,21 @@ pub enum ArgError { /// The argument is not the expected type. #[error("expected `{expected}` but received `{received}` (@ argument index {index})")] UnexpectedType { + /// Argument index. index: usize, + /// Expected argument type path. expected: Cow<'static, str>, + /// Received argument type path. received: Cow<'static, str>, }, /// The argument has the wrong ownership. #[error("expected {expected} value but received {received} value (@ argument index {index})")] InvalidOwnership { + /// Argument index. index: usize, + /// Expected ownership. expected: Ownership, + /// Received ownership. received: Ownership, }, /// Occurs when attempting to access an argument from an empty [`ArgList`]. diff --git a/crates/bevy_reflect/src/func/error.rs b/crates/bevy_reflect/src/func/error.rs index d9d105db1b..dc442e9da8 100644 --- a/crates/bevy_reflect/src/func/error.rs +++ b/crates/bevy_reflect/src/func/error.rs @@ -18,11 +18,18 @@ pub enum FunctionError { ArgError(#[from] ArgError), /// The number of arguments provided does not match the expected number. #[error("received {received} arguments but expected one of {expected:?}")] - ArgCountMismatch { expected: ArgCount, received: usize }, + ArgCountMismatch { + /// Expected argument count. [`ArgCount`] for overloaded functions will contain multiple possible counts. + expected: ArgCount, + /// Number of arguments received. + received: usize, + }, /// No overload was found for the given set of arguments. #[error("no overload found for arguments with signature `{received:?}`, expected one of `{expected:?}`")] NoOverload { + /// The set of available argument signatures. expected: HashSet, + /// The received argument signature. received: ArgumentSignature, }, } @@ -47,6 +54,9 @@ pub enum FunctionOverloadError { /// An error that occurs when attempting to add a function overload with a duplicate signature. #[error("could not add function overload: duplicate found for signature `{0:?}`")] DuplicateSignature(ArgumentSignature), + /// An attempt was made to add an overload with more than [`ArgCount::MAX_COUNT`] arguments. + /// + /// [`ArgCount::MAX_COUNT`]: crate::func::args::ArgCount #[error( "argument signature `{:?}` has too many arguments (max {})", 0, diff --git a/crates/bevy_reflect/src/func/info.rs b/crates/bevy_reflect/src/func/info.rs index 4b130e5772..2f5f82fbf5 100644 --- a/crates/bevy_reflect/src/func/info.rs +++ b/crates/bevy_reflect/src/func/info.rs @@ -235,6 +235,12 @@ impl TryFrom<[SignatureInfo; N]> for FunctionInfo { } } +/// Type information for the signature of a [`DynamicFunction`] or [`DynamicFunctionMut`]. +/// +/// Every [`FunctionInfo`] contains one or more [`SignatureInfo`]s. +/// +/// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug, Clone)] pub struct SignatureInfo { name: Option>, diff --git a/crates/bevy_reflect/src/func/mod.rs b/crates/bevy_reflect/src/func/mod.rs index 74a89282c6..237bc9eafc 100644 --- a/crates/bevy_reflect/src/func/mod.rs +++ b/crates/bevy_reflect/src/func/mod.rs @@ -42,7 +42,7 @@ //! //! A "function" is a callable that does not capture its environment. //! These are typically defined with the `fn` keyword, which are referred to as _named_ functions. -//! But they are also _anonymous_ functions, which are unnamed and defined with anonymous function syntax. +//! But there are also _anonymous_ functions, which are unnamed and defined with anonymous function syntax. //! //! ```rust //! // This is a named function: diff --git a/crates/bevy_reflect/src/func/registry.rs b/crates/bevy_reflect/src/func/registry.rs index e476353b8b..08ed7bd7f1 100644 --- a/crates/bevy_reflect/src/func/registry.rs +++ b/crates/bevy_reflect/src/func/registry.rs @@ -336,6 +336,7 @@ impl Debug for FunctionRegistry { /// A synchronized wrapper around a [`FunctionRegistry`]. #[derive(Clone, Default, Debug)] pub struct FunctionRegistryArc { + /// The wrapped [`FunctionRegistry`]. pub internal: Arc>, } diff --git a/crates/bevy_reflect/src/kind.rs b/crates/bevy_reflect/src/kind.rs index 3eef10d0e5..3764e863ba 100644 --- a/crates/bevy_reflect/src/kind.rs +++ b/crates/bevy_reflect/src/kind.rs @@ -134,7 +134,9 @@ macro_rules! impl_reflect_kind_conversions { #[derive(Debug, Error)] #[error("kind mismatch: expected {expected:?}, received {received:?}")] pub struct ReflectKindMismatchError { + /// Expected kind. pub expected: ReflectKind, + /// Received kind. pub received: ReflectKind, } @@ -176,16 +178,46 @@ macro_rules! impl_cast_method { /// /// ["kinds"]: ReflectKind pub enum ReflectRef<'a> { + /// An immutable reference to a [struct-like] type. + /// + /// [struct-like]: Struct Struct(&'a dyn Struct), + /// An immutable reference to a [tuple-struct-like] type. + /// + /// [tuple-struct-like]: TupleStruct TupleStruct(&'a dyn TupleStruct), + /// An immutable reference to a [tuple-like] type. + /// + /// [tuple-like]: Tuple Tuple(&'a dyn Tuple), + /// An immutable reference to a [list-like] type. + /// + /// [list-like]: List List(&'a dyn List), + /// An immutable reference to an [array-like] type. + /// + /// [array-like]: Array Array(&'a dyn Array), + /// An immutable reference to a [map-like] type. + /// + /// [map-like]: Map Map(&'a dyn Map), + /// An immutable reference to a [set-like] type. + /// + /// [set-like]: Set Set(&'a dyn Set), + /// An immutable reference to an [enum-like] type. + /// + /// [enum-like]: Enum Enum(&'a dyn Enum), + /// An immutable reference to a [function-like] type. + /// + /// [function-like]: Function #[cfg(feature = "functions")] Function(&'a dyn Function), + /// An immutable refeence to an [opaque] type. + /// + /// [opaque]: ReflectKind::Opaque Opaque(&'a dyn PartialReflect), } impl_reflect_kind_conversions!(ReflectRef<'_>); @@ -211,16 +243,46 @@ impl<'a> ReflectRef<'a> { /// /// ["kinds"]: ReflectKind pub enum ReflectMut<'a> { + /// A mutable reference to a [struct-like] type. + /// + /// [struct-like]: Struct Struct(&'a mut dyn Struct), + /// A mutable reference to a [tuple-struct-like] type. + /// + /// [tuple-struct-like]: TupleStruct TupleStruct(&'a mut dyn TupleStruct), + /// A mutable reference to a [tuple-like] type. + /// + /// [tuple-like]: Tuple Tuple(&'a mut dyn Tuple), + /// A mutable reference to a [list-like] type. + /// + /// [list-like]: List List(&'a mut dyn List), + /// A mutable reference to an [array-like] type. + /// + /// [array-like]: Array Array(&'a mut dyn Array), + /// A mutable reference to a [map-like] type. + /// + /// [map-like]: Map Map(&'a mut dyn Map), + /// A mutable reference to a [set-like] type. + /// + /// [set-like]: Set Set(&'a mut dyn Set), + /// A mutable reference to an [enum-like] type. + /// + /// [enum-like]: Enum Enum(&'a mut dyn Enum), #[cfg(feature = "functions")] + /// A mutable reference to a [function-like] type. + /// + /// [function-like]: Function Function(&'a mut dyn Function), + /// A mutable refeence to an [opaque] type. + /// + /// [opaque]: ReflectKind::Opaque Opaque(&'a mut dyn PartialReflect), } impl_reflect_kind_conversions!(ReflectMut<'_>); @@ -246,16 +308,46 @@ impl<'a> ReflectMut<'a> { /// /// ["kinds"]: ReflectKind pub enum ReflectOwned { + /// An owned [struct-like] type. + /// + /// [struct-like]: Struct Struct(Box), + /// An owned [tuple-struct-like] type. + /// + /// [tuple-struct-like]: TupleStruct TupleStruct(Box), + /// An owned [tuple-like] type. + /// + /// [tuple-like]: Tuple Tuple(Box), + /// An owned [list-like] type. + /// + /// [list-like]: List List(Box), + /// An owned [array-like] type. + /// + /// [array-like]: Array Array(Box), + /// An owned [map-like] type. + /// + /// [map-like]: Map Map(Box), + /// An owned [set-like] type. + /// + /// [set-like]: Set Set(Box), + /// An owned [enum-like] type. + /// + /// [enum-like]: Enum Enum(Box), + /// An owned [function-like] type. + /// + /// [function-like]: Function #[cfg(feature = "functions")] Function(Box), + /// An owned [opaque] type. + /// + /// [opaque]: ReflectKind::Opaque Opaque(Box), } impl_reflect_kind_conversions!(ReflectOwned); diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index 0f399afd59..eabfdc0eac 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -1,4 +1,3 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr( any(docsrs, docsrs_dep), expect( diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index 1a1fcefb63..20531569e8 100644 --- a/crates/bevy_reflect/src/map.rs +++ b/crates/bevy_reflect/src/map.rs @@ -192,6 +192,8 @@ impl MapInfo { impl_generic_info_methods!(generics); } +/// Used to produce an error message when an attempt is made to hash +/// a [`PartialReflect`] value that does not support hashing. #[macro_export] macro_rules! hash_error { ( $key:expr ) => {{ diff --git a/crates/bevy_reflect/src/reflect.rs b/crates/bevy_reflect/src/reflect.rs index 04e4a2a4b0..c1e283a5f4 100644 --- a/crates/bevy_reflect/src/reflect.rs +++ b/crates/bevy_reflect/src/reflect.rs @@ -21,32 +21,47 @@ pub enum ApplyError { #[error("attempted to apply `{from_kind}` to `{to_kind}`")] /// Attempted to apply the wrong [kind](ReflectKind) to a type, e.g. a struct to an enum. MismatchedKinds { + /// Kind of the value we attempted to apply. from_kind: ReflectKind, + /// Kind of the type we attempted to apply the value to. to_kind: ReflectKind, }, #[error("enum variant `{variant_name}` doesn't have a field named `{field_name}`")] /// Enum variant that we tried to apply to was missing a field. MissingEnumField { + /// Name of the enum variant. variant_name: Box, + /// Name of the missing field. field_name: Box, }, #[error("`{from_type}` is not `{to_type}`")] /// Tried to apply incompatible types. MismatchedTypes { + /// Type of the value we attempted to apply. from_type: Box, + /// Type we attempted to apply the value to. to_type: Box, }, #[error("attempted to apply type with {from_size} size to a type with {to_size} size")] - /// Attempted to apply to types with mismatched sizes, e.g. a [u8; 4] to [u8; 3]. - DifferentSize { from_size: usize, to_size: usize }, + /// Attempted to apply an [array-like] type to another of different size, e.g. a [u8; 4] to [u8; 3]. + /// + /// [array-like]: crate::Array + DifferentSize { + /// Size of the value we attempted to apply, in elements. + from_size: usize, + /// Size of the type we attempted to apply the value to, in elements. + to_size: usize, + }, #[error("variant with name `{variant_name}` does not exist on enum `{enum_name}`")] /// The enum we tried to apply to didn't contain a variant with the give name. UnknownVariant { + /// Name of the enum. enum_name: Box, + /// Name of the missing variant. variant_name: Box, }, } diff --git a/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs b/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs index f92a8e68e2..8a216f87b9 100644 --- a/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs +++ b/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs @@ -42,6 +42,9 @@ use serde::Deserializer; /// [`ReflectDeserializer`]: crate::serde::ReflectDeserializer /// [via the registry]: TypeRegistry::register_type_data pub trait DeserializeWithRegistry<'de>: Sized { + /// Deserialize this value using the given [Deserializer] and [`TypeRegistry`]. + /// + /// [`Deserializer`]: ::serde::Deserializer fn deserialize(deserializer: D, registry: &TypeRegistry) -> Result where D: Deserializer<'de>; diff --git a/crates/bevy_reflect/src/serde/de/registrations.rs b/crates/bevy_reflect/src/serde/de/registrations.rs index adc0025c54..768b8ed32f 100644 --- a/crates/bevy_reflect/src/serde/de/registrations.rs +++ b/crates/bevy_reflect/src/serde/de/registrations.rs @@ -15,6 +15,7 @@ pub struct TypeRegistrationDeserializer<'a> { } impl<'a> TypeRegistrationDeserializer<'a> { + /// Creates a new [`TypeRegistrationDeserializer`]. pub fn new(registry: &'a TypeRegistry) -> Self { Self { registry } } diff --git a/crates/bevy_reflect/src/serde/mod.rs b/crates/bevy_reflect/src/serde/mod.rs index 032590e0c7..2ee47d4a7f 100644 --- a/crates/bevy_reflect/src/serde/mod.rs +++ b/crates/bevy_reflect/src/serde/mod.rs @@ -1,3 +1,5 @@ +//! Serde integration for reflected types. + mod de; mod ser; mod type_data; diff --git a/crates/bevy_reflect/src/serde/ser/serializable.rs b/crates/bevy_reflect/src/serde/ser/serializable.rs index 6a8a4c978f..c83737c842 100644 --- a/crates/bevy_reflect/src/serde/ser/serializable.rs +++ b/crates/bevy_reflect/src/serde/ser/serializable.rs @@ -3,7 +3,9 @@ use core::ops::Deref; /// A type-erased serializable value. pub enum Serializable<'a> { + /// An owned serializable value. Owned(Box), + /// An immutable reference to a serializable value. Borrowed(&'a dyn erased_serde::Serialize), } diff --git a/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs b/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs index 9c5bfb06f1..f9e6370799 100644 --- a/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs +++ b/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs @@ -40,6 +40,9 @@ use serde::{Serialize, Serializer}; /// [`ReflectSerializer`]: crate::serde::ReflectSerializer /// [via the registry]: TypeRegistry::register_type_data pub trait SerializeWithRegistry { + /// Serialize this value using the given [Serializer] and [`TypeRegistry`]. + /// + /// [`Serializer`]: ::serde::Serializer fn serialize(&self, serializer: S, registry: &TypeRegistry) -> Result where S: Serializer; diff --git a/crates/bevy_reflect/src/std_traits.rs b/crates/bevy_reflect/src/std_traits.rs index cad001132b..9b7f46c300 100644 --- a/crates/bevy_reflect/src/std_traits.rs +++ b/crates/bevy_reflect/src/std_traits.rs @@ -1,3 +1,5 @@ +//! Module containing the [`ReflectDefault`] type. + use crate::{FromType, Reflect}; use alloc::boxed::Box; @@ -10,6 +12,7 @@ pub struct ReflectDefault { } impl ReflectDefault { + /// Returns the default value for a type. pub fn default(&self) -> Box { (self.default)() } diff --git a/crates/bevy_reflect/src/struct_trait.rs b/crates/bevy_reflect/src/struct_trait.rs index 4346f55e27..e419947b3a 100644 --- a/crates/bevy_reflect/src/struct_trait.rs +++ b/crates/bevy_reflect/src/struct_trait.rs @@ -71,6 +71,7 @@ pub trait Struct: PartialReflect { /// Returns an iterator over the values of the reflectable fields for this struct. fn iter_fields(&self) -> FieldIter; + /// Creates a new [`DynamicStruct`] from this struct. fn to_dynamic_struct(&self) -> DynamicStruct { let mut dynamic_struct = DynamicStruct::default(); dynamic_struct.set_represented_type(self.get_represented_type_info()); @@ -192,6 +193,7 @@ pub struct FieldIter<'a> { } impl<'a> FieldIter<'a> { + /// Creates a new [`FieldIter`]. pub fn new(value: &'a dyn Struct) -> Self { FieldIter { struct_val: value, diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index 8bdd08099b..51f402c698 100644 --- a/crates/bevy_reflect/src/tuple.rs +++ b/crates/bevy_reflect/src/tuple.rs @@ -76,6 +76,7 @@ pub struct TupleFieldIter<'a> { } impl<'a> TupleFieldIter<'a> { + /// Creates a new [`TupleFieldIter`]. pub fn new(value: &'a dyn Tuple) -> Self { TupleFieldIter { tuple: value, diff --git a/crates/bevy_reflect/src/tuple_struct.rs b/crates/bevy_reflect/src/tuple_struct.rs index ab5b99a96b..cceab9904e 100644 --- a/crates/bevy_reflect/src/tuple_struct.rs +++ b/crates/bevy_reflect/src/tuple_struct.rs @@ -146,6 +146,7 @@ pub struct TupleStructFieldIter<'a> { } impl<'a> TupleStructFieldIter<'a> { + /// Creates a new [`TupleStructFieldIter`]. pub fn new(value: &'a dyn TupleStruct) -> Self { TupleStructFieldIter { tuple_struct: value, diff --git a/crates/bevy_reflect/src/type_info.rs b/crates/bevy_reflect/src/type_info.rs index 1a3be15c36..122ace0293 100644 --- a/crates/bevy_reflect/src/type_info.rs +++ b/crates/bevy_reflect/src/type_info.rs @@ -169,7 +169,9 @@ pub enum TypeInfoError { /// [kind]: ReflectKind #[error("kind mismatch: expected {expected:?}, received {received:?}")] KindMismatch { + /// Expected kind. expected: ReflectKind, + /// Received kind. received: ReflectKind, }, } @@ -183,7 +185,7 @@ pub enum TypeInfoError { /// 3. [`PartialReflect::get_represented_type_info`] /// 4. [`TypeRegistry::get_type_info`] /// -/// Each return a static reference to [`TypeInfo`], but they all have their own use cases. +/// Each returns a static reference to [`TypeInfo`], but they all have their own use cases. /// For example, if you know the type at compile time, [`Typed::type_info`] is probably /// the simplest. If you have a `dyn Reflect` you can use [`DynamicTyped::reflect_type_info`]. /// If all you have is a `dyn PartialReflect`, you'll probably want [`PartialReflect::get_represented_type_info`]. @@ -199,14 +201,40 @@ pub enum TypeInfoError { /// [type path]: TypePath::type_path #[derive(Debug, Clone)] pub enum TypeInfo { + /// Type information for a [struct-like] type. + /// + /// [struct-like]: crate::Struct Struct(StructInfo), + /// Type information for a [tuple-struct-like] type. + /// + /// [tuple-struct-like]: crate::TupleStruct TupleStruct(TupleStructInfo), + /// Type information for a [tuple-like] type. + /// + /// [tuple-like]: crate::Tuple Tuple(TupleInfo), + /// Type information for a [list-like] type. + /// + /// [list-like]: crate::List List(ListInfo), + /// Type information for an [array-like] type. + /// + /// [array-like]: crate::Array Array(ArrayInfo), + /// Type information for a [map-like] type. + /// + /// [map-like]: crate::Map Map(MapInfo), + /// Type information for a [set-like] type. + /// + /// [set-like]: crate::Set Set(SetInfo), + /// Type information for an [enum-like] type. + /// + /// [enum-like]: crate::Enum Enum(EnumInfo), + /// Type information for an opaque type - see the [`OpaqueInfo`] docs for + /// a discussion of opaque types. Opaque(OpaqueInfo), } @@ -557,6 +585,7 @@ pub struct OpaqueInfo { } impl OpaqueInfo { + /// Creates a new [`OpaqueInfo`]. pub fn new() -> Self { Self { ty: Type::of::(), diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index 5827ebdac5..1f18396be2 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -38,6 +38,7 @@ pub struct TypeRegistry { /// A synchronized wrapper around a [`TypeRegistry`]. #[derive(Clone, Default)] pub struct TypeRegistryArc { + /// The wrapped [`TypeRegistry`]. pub internal: Arc>, } @@ -313,6 +314,7 @@ impl TypeRegistry { data.insert(D::from_type()); } + /// Whether the type with given [`TypeId`] has been registered in this registry. pub fn contains(&self, type_id: TypeId) -> bool { self.registrations.contains_key(&type_id) } @@ -684,6 +686,7 @@ impl Clone for TypeRegistration { /// /// [crate-level documentation]: crate pub trait TypeData: Downcast + Send + Sync { + /// Creates a type-erased clone of this value. fn clone_type_data(&self) -> Box; } impl_downcast!(TypeData); @@ -702,6 +705,7 @@ where /// This is used by the `#[derive(Reflect)]` macro to generate an implementation /// of [`TypeData`] to pass to [`TypeRegistration::insert`]. pub trait FromType { + /// Creates an instance of `Self` for type `T`. fn from_type() -> Self; } @@ -746,6 +750,8 @@ impl ReflectSerialize { /// [`FromType::from_type`]. #[derive(Clone)] pub struct ReflectDeserialize { + /// Function used by [`ReflectDeserialize::deserialize`] to + /// perform deserialization. pub func: fn( deserializer: &mut dyn erased_serde::Deserializer, ) -> Result, erased_serde::Error>, diff --git a/crates/bevy_reflect/src/utility.rs b/crates/bevy_reflect/src/utility.rs index 5735a29dbe..db8416bd6c 100644 --- a/crates/bevy_reflect/src/utility.rs +++ b/crates/bevy_reflect/src/utility.rs @@ -16,6 +16,7 @@ use core::{ /// /// [`Non`]: NonGenericTypeCell pub trait TypedProperty: sealed::Sealed { + /// The type of the value stored in [`GenericTypeCell`]. type Stored: 'static; } @@ -201,7 +202,7 @@ impl Default for NonGenericTypeCell { /// static CELL: GenericTypePathCell = GenericTypePathCell::new(); /// CELL.get_or_insert::(|| format!("my_crate::foo::Foo<{}>", T::type_path())) /// } -/// +/// /// fn short_type_path() -> &'static str { /// static CELL: GenericTypePathCell = GenericTypePathCell::new(); /// CELL.get_or_insert::(|| format!("Foo<{}>", T::short_type_path())) From 8661e914a57d4e5c8bec20f0a840041c0e8aea25 Mon Sep 17 00:00:00 2001 From: SilentSpaceTraveller Date: Mon, 16 Jun 2025 23:30:55 +0200 Subject: [PATCH 040/106] bevy_log: refactor how log layers are wired together (#19248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Current way to wire `Layer`s together using `layer.with(new_layer)` in the `bevy_log` plugin is brittle and not flexible. As #17722 demonstrated, the current solution makes it very hard to do any kind of advanced wiring, as the type system of `tracing::Subscriber` gets in the way very quickly (the type of each new layer depends on the type of the previous ones). We want to make it easier to have more complex wiring of `Layers`. It would be hard to solve #19085 without it ## Solution It aims to be functionally equivalent. - Replace of using `layer.with(new_layer)` . We now add `layer.boxed()` to a `Vec`. It is a solution recommended by `tracing_subscriber::Layer` for complex wiring cases (See https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/index.html#runtime-configuration-with-layers) - Do some refactoring and clean up that is now enabled by the new solution ## Testing - Ran CI locally on Linux - Ran the logs examples - Need people familiar with the features `trace`, `tracing-chrome`, `tracing-tracy` to check that it still works as expected - Need people with access to `ios`, `android` and `wasm` to check it as well. --------- Co-authored-by: Alice Cecile Co-authored-by: Kristoffer Søholm --- crates/bevy_log/src/lib.rs | 237 +++++++++++++++++++++---------------- examples/app/log_layers.rs | 11 +- 2 files changed, 143 insertions(+), 105 deletions(-) diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index 7a80a21cc3..c0ad83468d 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -56,7 +56,6 @@ use bevy_app::{App, Plugin}; use tracing_log::LogTracer; use tracing_subscriber::{ filter::{FromEnvError, ParseError}, - layer::Layered, prelude::*, registry::Registry, EnvFilter, Layer, @@ -84,6 +83,8 @@ pub(crate) struct FlushGuard(SyncCell); /// logging to `stdout`. /// * Using [`android_log-sys`](https://crates.io/crates/android_log-sys) on Android, /// logging to Android logs. +/// * Using [`tracing_oslog`](https://crates.io/crates/tracing_oslog) on iOS, +/// logging to iOS logs. /// * Using [`tracing-wasm`](https://crates.io/crates/tracing-wasm) in Wasm, logging /// to the browser console. /// @@ -254,26 +255,15 @@ pub struct LogPlugin { /// timestamp from the log output. /// /// Please see the `examples/log_layers.rs` for a complete example. - pub fmt_layer: fn(app: &mut App) -> Option, + /// + /// Note that this field has no effect when `os_target` is `android`, `ios` or `wasm`, as on those + /// platforms we don't use [`tracing_subscriber::fmt::Layer`] but rather the platform default. + pub fmt_layer: fn(app: &mut App) -> Option, } /// A boxed [`Layer`] that can be used with [`LogPlugin::custom_layer`]. pub type BoxedLayer = Box + Send + Sync + 'static>; -#[cfg(feature = "trace")] -type BaseSubscriber = - Layered + Send + Sync>>, Registry>>; - -#[cfg(feature = "trace")] -type PreFmtSubscriber = Layered, BaseSubscriber>; - -#[cfg(not(feature = "trace"))] -type PreFmtSubscriber = - Layered + Send + Sync>>, Registry>>; - -/// A boxed [`Layer`] that can be used with [`LogPlugin::fmt_layer`]. -pub type BoxedFmtLayer = Box + Send + Sync + 'static>; - /// The default [`LogPlugin`] [`EnvFilter`]. pub const DEFAULT_FILTER: &str = "wgpu=error,naga=warn"; @@ -300,30 +290,25 @@ impl Plugin for LogPlugin { })); } - let finished_subscriber; - let subscriber = Registry::default(); + // We use a Vec of BoxedLayer instead of adding each layer individually using the + // `layer.with(next_layer)`. + // Otherwise, the types of each successive layer becomes unwieldy, + // as the type of each new layer would depend on the types of the previous layers. + let mut layers: Vec = Vec::new(); - // add optional layer provided by user - let subscriber = subscriber.with((self.custom_layer)(app)); + // Add optional layer provided by user + // As they are added first, any of the following layers won't be applied. + // In particular, it won't be affected by the filtering we put in place next. + if let Some(layer) = (self.custom_layer)(app) { + layers.push(layer); + } - let default_filter = { format!("{},{}", self.level, self.filter) }; - let filter_layer = EnvFilter::try_from_default_env() - .or_else(|from_env_error| { - _ = from_env_error - .source() - .and_then(|source| source.downcast_ref::()) - .map(|parse_err| { - // we cannot use the `error!` macro here because the logger is not ready yet. - eprintln!("LogPlugin failed to parse filter from env: {}", parse_err); - }); - - Ok::(EnvFilter::builder().parse_lossy(&default_filter)) - }) - .unwrap(); - let subscriber = subscriber.with(filter_layer); + layers.push(Self::build_filter_layer(self.level, &self.filter)); #[cfg(feature = "trace")] - let subscriber = subscriber.with(tracing_error::ErrorLayer::default()); + layers.push(tracing_error::ErrorLayer::default().boxed()); + + layers.push(Self::build_system_output_layer((self.fmt_layer)(app))); #[cfg(all( not(target_arch = "wasm32"), @@ -332,76 +317,19 @@ impl Plugin for LogPlugin { ))] { #[cfg(feature = "tracing-chrome")] - let chrome_layer = { - let mut layer = tracing_chrome::ChromeLayerBuilder::new(); - if let Ok(path) = std::env::var("TRACE_CHROME") { - layer = layer.file(path); - } - let (chrome_layer, guard) = layer - .name_fn(Box::new(|event_or_span| match event_or_span { - tracing_chrome::EventOrSpan::Event(event) => event.metadata().name().into(), - tracing_chrome::EventOrSpan::Span(span) => { - if let Some(fields) = - span.extensions().get::>() - { - format!("{}: {}", span.metadata().name(), fields.fields.as_str()) - } else { - span.metadata().name().into() - } - } - })) - .build(); + { + let (chrome_layer, guard) = Self::build_chrome_layer(); app.insert_resource(FlushGuard(SyncCell::new(guard))); - chrome_layer - }; - + layers.push(chrome_layer); + } #[cfg(feature = "tracing-tracy")] - let tracy_layer = tracing_tracy::TracyLayer::default(); - - let fmt_layer = (self.fmt_layer)(app).unwrap_or_else(|| { - // note: the implementation of `Default` reads from the env var NO_COLOR - // to decide whether to use ANSI color codes, which is common convention - // https://no-color.org/ - Box::new(tracing_subscriber::fmt::Layer::default().with_writer(std::io::stderr)) - }); - - // bevy_render::renderer logs a `tracy.frame_mark` event every frame - // at Level::INFO. Formatted logs should omit it. - #[cfg(feature = "tracing-tracy")] - let fmt_layer = - fmt_layer.with_filter(tracing_subscriber::filter::FilterFn::new(|meta| { - meta.fields().field("tracy.frame_mark").is_none() - })); - - let subscriber = subscriber.with(fmt_layer); - - #[cfg(feature = "tracing-chrome")] - let subscriber = subscriber.with(chrome_layer); - #[cfg(feature = "tracing-tracy")] - let subscriber = subscriber.with(tracy_layer); - finished_subscriber = subscriber; + layers.push(tracing_tracy::TracyLayer::default().boxed()); } - #[cfg(target_arch = "wasm32")] - { - finished_subscriber = subscriber.with(tracing_wasm::WASMLayer::new( - tracing_wasm::WASMLayerConfig::default(), - )); - } - - #[cfg(target_os = "android")] - { - finished_subscriber = subscriber.with(android_tracing::AndroidLayer::default()); - } - - #[cfg(target_os = "ios")] - { - finished_subscriber = subscriber.with(tracing_oslog::OsLogger::default()); - } + let subscriber = Registry::default().with(layers); let logger_already_set = LogTracer::init().is_err(); - let subscriber_already_set = - tracing::subscriber::set_global_default(finished_subscriber).is_err(); + let subscriber_already_set = tracing::subscriber::set_global_default(subscriber).is_err(); match (logger_already_set, subscriber_already_set) { (true, true) => error!( @@ -413,3 +341,112 @@ impl Plugin for LogPlugin { } } } + +impl LogPlugin { + /// Build a [`BoxedLayer`] that will filter which logs are outputted. + /// It will read the `RUST_LOG` env variable to override the settings + /// on a given run, the default will fallback to the provided `level` and `filter` + fn build_filter_layer(level: Level, filter: &str) -> BoxedLayer { + let default_filter = { format!("{},{}", level, filter) }; + + EnvFilter::try_from_default_env() + .or_else(|from_env_error| { + _ = from_env_error + .source() + .and_then(|source| source.downcast_ref::()) + .map(|parse_err| { + #[expect( + clippy::print_stderr, + reason = "We cannot use the `error!` macro here because the logger is not ready yet." + )] + { + eprintln!("LogPlugin failed to parse filter from env: {}", parse_err); + } + }); + + Ok::(EnvFilter::builder().parse_lossy(&default_filter)) + }) + .unwrap().boxed() + } + + #[cfg(feature = "tracing-chrome")] + /// [`BoxedLayer`] to build the necessary output when the `tracing-chrome` feature is enabled. + /// The [`tracing_chrome::FlushGuard`] must be kept around till we don't need to output logs + /// any more + fn build_chrome_layer() -> (BoxedLayer, tracing_chrome::FlushGuard) { + let mut layer = tracing_chrome::ChromeLayerBuilder::new(); + if let Ok(path) = std::env::var("TRACE_CHROME") { + layer = layer.file(path); + } + let (chrome_layer, guard) = layer + .name_fn(Box::new(|event_or_span| match event_or_span { + tracing_chrome::EventOrSpan::Event(event) => event.metadata().name().into(), + tracing_chrome::EventOrSpan::Span(span) => { + if let Some(fields) = span.extensions().get::>() + { + format!("{}: {}", span.metadata().name(), fields.fields.as_str()) + } else { + span.metadata().name().into() + } + } + })) + .build(); + (chrome_layer.boxed(), guard) + } + + #[expect( + clippy::allow_attributes, + reason = "We can't switch to `expect` for allow(unused_variables) as we use it if not on those platforms" + )] + #[allow(unused_variables, reason = "Not used on `wasm32`, `android` or `ios")] + /// Build a [`BoxedLayer`] that outputs logs to the system default. + /// On most platforms, it will be `stderr` with [`tracing_subscriber::fmt::Layer`], expect on `android`, `ios` and` wasm32` where it + /// uses those system default log infrastructure. + /// It is possible to override how you output those logs by providing a `custom_format_layer`. + /// Note that won't have an effect on platform that don't use [`tracing_subscriber::fmt::Layer`] + fn build_system_output_layer(custom_format_layer: Option) -> BoxedLayer { + let layer: BoxedLayer; + #[cfg(target_arch = "wasm32")] + { + layer = tracing_wasm::WASMLayer::new(tracing_wasm::WASMLayerConfig::default()).boxed(); + } + + #[cfg(target_os = "android")] + { + layer = android_tracing::AndroidLayer::default().boxed(); + } + + #[cfg(target_os = "ios")] + { + layer = tracing_oslog::OsLogger::default().boxed(); + } + + #[cfg(all( + not(target_arch = "wasm32"), + not(target_os = "android"), + not(target_os = "ios") + ))] + { + layer = { + let fmt_layer = custom_format_layer.unwrap_or_else(|| { + tracing_subscriber::fmt::Layer::default() + // note: the implementation of `Default` reads from the env var NO_COLOR + // to decide whether to use ANSI color codes, which is common convention + // https://no-color.org/ + .with_writer(std::io::stderr) + .boxed() + }); + + // bevy_render::renderer logs a `tracy.frame_mark` event every frame + // at Level::INFO. Formatted logs should omit it. + #[cfg(feature = "tracing-tracy")] + let fmt_layer = + fmt_layer.with_filter(tracing_subscriber::filter::FilterFn::new(|meta| { + meta.fields().field("tracy.frame_mark").is_none() + })); + fmt_layer.boxed() + } + } + layer + } +} diff --git a/examples/app/log_layers.rs b/examples/app/log_layers.rs index 8f454e9ee0..83e17ef107 100644 --- a/examples/app/log_layers.rs +++ b/examples/app/log_layers.rs @@ -4,7 +4,7 @@ use bevy::{ log::{ tracing::{self, Subscriber}, tracing_subscriber::Layer, - BoxedFmtLayer, BoxedLayer, + BoxedLayer, }, prelude::*, }; @@ -41,12 +41,13 @@ fn custom_layer(_app: &mut App) -> Option { // `fmt_layer` option. // // In this example, we're disabling the timestamp in the log output. -fn fmt_layer(_app: &mut App) -> Option { - Some(Box::new( +fn fmt_layer(_app: &mut App) -> Option { + Some( bevy::log::tracing_subscriber::fmt::Layer::default() .without_time() - .with_writer(std::io::stderr), - )) + .with_writer(std::io::stderr) + .boxed(), + ) } fn main() { From 1f2fd3d29dfd854f25ae917ebe1c63d727a16340 Mon Sep 17 00:00:00 2001 From: mgi388 <135186256+mgi388@users.noreply.github.com> Date: Tue, 17 Jun 2025 07:34:22 +1000 Subject: [PATCH 041/106] Fix SubStates with multiple source states not reacting to all source changes (#19595) # Objective - Fix issue where `SubStates` depending on multiple source states would only react when _all_ source states changed simultaneously. - SubStates should be created/destroyed whenever _any_ of their source states transitions, not only when all change together. # Solution - Changed the "did parent change" detection logic from AND to OR. We need to check if _any_ of the event readers changed, not if _all_ of them changed. - See https://github.com/bevyengine/bevy/actions/runs/15610159742/job/43968937544?pr=19595 for failing test proof before I pushed the fix. - The generated code we want needs `||`s not `&&`s like this: ```rust fn register_sub_state_systems_in_schedule>(schedule: &mut Schedule) { let apply_state_transition = |(mut ereader0, mut ereader1, mut ereader2): ( EventReader>, EventReader>, EventReader>, ), event: EventWriter>, commands: Commands, current_state_res: Option>>, next_state_res: Option>>, (s0, s1, s2): ( Option>>, Option>>, Option>>, )| { // With `||` we can correctly count parent changed if any of the sources changed. let parent_changed = (ereader0.read().last().is_some() || ereader1.read().last().is_some() || ereader2.read().last().is_some()); let next_state = take_next_state(next_state_res); if !parent_changed && next_state.is_none() { return; } // ... } } ``` # Testing - Add new test. - Check the fix worked in my game. --- crates/bevy_state/src/state/mod.rs | 197 +++++++++++++++++++++++ crates/bevy_state/src/state/state_set.rs | 2 +- 2 files changed, 198 insertions(+), 1 deletion(-) diff --git a/crates/bevy_state/src/state/mod.rs b/crates/bevy_state/src/state/mod.rs index 9267478281..61ee0627a3 100644 --- a/crates/bevy_state/src/state/mod.rs +++ b/crates/bevy_state/src/state/mod.rs @@ -642,6 +642,203 @@ mod tests { } } + #[derive(PartialEq, Eq, Debug, Hash, Clone)] + enum MultiSourceComputedState { + FromSimpleBTrue, + FromSimple2B2, + FromBoth, + } + + impl ComputedStates for MultiSourceComputedState { + type SourceStates = (SimpleState, SimpleState2); + + fn compute((simple_state, simple_state2): (SimpleState, SimpleState2)) -> Option { + match (simple_state, simple_state2) { + // If both are in their special states, prioritize the "both" variant. + (SimpleState::B(true), SimpleState2::B2) => Some(Self::FromBoth), + // If only SimpleState is B(true). + (SimpleState::B(true), _) => Some(Self::FromSimpleBTrue), + // If only SimpleState2 is B2. + (_, SimpleState2::B2) => Some(Self::FromSimple2B2), + // Otherwise, no computed state. + _ => None, + } + } + } + + /// This test ensures that [`ComputedStates`] with multiple source states + /// react when any source changes. + #[test] + fn computed_state_with_multiple_sources_should_react_to_any_source_change() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + + world.init_resource::>(); + world.init_resource::>(); + + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + SimpleState::register_state(&mut apply_changes); + SimpleState2::register_state(&mut apply_changes); + MultiSourceComputedState::register_computed_state_systems(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + setup_state_transitions_in_world(&mut world); + + // Initial state: SimpleState::A, SimpleState2::A1 and + // MultiSourceComputedState should not exist yet. + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::A1); + assert!(!world.contains_resource::>()); + + // Change only SimpleState to B(true) - this should trigger + // MultiSourceComputedState. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SimpleState2::A1); + // The computed state should exist because SimpleState changed to + // B(true). + assert!(world.contains_resource::>()); + assert_eq!( + world.resource::>().0, + MultiSourceComputedState::FromSimpleBTrue + ); + + // Reset SimpleState to A - computed state should be removed. + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + + // Now change only SimpleState2 to B2 - this should also trigger + // MultiSourceComputedState. + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::B2); + // The computed state should exist because SimpleState2 changed to B2. + assert!(world.contains_resource::>()); + assert_eq!( + world.resource::>().0, + MultiSourceComputedState::FromSimple2B2 + ); + + // Test that changes to both states work. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::A1)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + MultiSourceComputedState::FromSimpleBTrue + ); + } + + // Test SubState that depends on multiple source states. + #[derive(PartialEq, Eq, Debug, Default, Hash, Clone)] + enum MultiSourceSubState { + #[default] + Active, + } + + impl SubStates for MultiSourceSubState { + type SourceStates = (SimpleState, SimpleState2); + + fn should_exist( + (simple_state, simple_state2): (SimpleState, SimpleState2), + ) -> Option { + // SubState should exist when: + // - SimpleState is B(true), OR + // - SimpleState2 is B2 + match (simple_state, simple_state2) { + (SimpleState::B(true), _) | (_, SimpleState2::B2) => Some(Self::Active), + _ => None, + } + } + } + + impl States for MultiSourceSubState { + const DEPENDENCY_DEPTH: usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; + } + + impl FreelyMutableState for MultiSourceSubState {} + + /// This test ensures that [`SubStates`] with multiple source states react + /// when any source changes. + #[test] + fn sub_state_with_multiple_sources_should_react_to_any_source_change() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + + world.init_resource::>(); + world.init_resource::>(); + + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + SimpleState::register_state(&mut apply_changes); + SimpleState2::register_state(&mut apply_changes); + MultiSourceSubState::register_sub_state_systems(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + setup_state_transitions_in_world(&mut world); + + // Initial state: SimpleState::A, SimpleState2::A1 and + // MultiSourceSubState should not exist yet. + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::A1); + assert!(!world.contains_resource::>()); + + // Change only SimpleState to B(true) - this should trigger + // MultiSourceSubState. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SimpleState2::A1); + // The sub state should exist because SimpleState changed to B(true). + assert!(world.contains_resource::>()); + + // Reset to initial state. + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + + // Now change only SimpleState2 to B2 - this should also trigger + // MultiSourceSubState creation. + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::B2); + // The sub state should exist because SimpleState2 changed to B2. + assert!(world.contains_resource::>()); + + // Finally, test that it works when both change simultaneously. + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.insert_resource(NextState::Pending(SimpleState2::A1)); + world.run_schedule(StateTransition); + // After this transition, the state should not exist since SimpleState + // is B(false). + assert!(!world.contains_resource::>()); + + // Change both at the same time. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert!(world.contains_resource::>()); + } + #[test] fn check_transition_orders() { let mut world = World::new(); diff --git a/crates/bevy_state/src/state/state_set.rs b/crates/bevy_state/src/state/state_set.rs index 69a6c41b3d..3cf1e1d260 100644 --- a/crates/bevy_state/src/state/state_set.rs +++ b/crates/bevy_state/src/state/state_set.rs @@ -293,7 +293,7 @@ macro_rules! impl_state_set_sealed_tuples { current_state_res: Option>>, next_state_res: Option>>, ($($val),*,): ($(Option>>),*,)| { - let parent_changed = ($($evt.read().last().is_some())&&*); + let parent_changed = ($($evt.read().last().is_some())||*); let next_state = take_next_state(next_state_res); if !parent_changed && next_state.is_none() { From 9b743d2a430e6770509bb4464fcbed5419ce56d5 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Mon, 16 Jun 2025 23:47:34 +0200 Subject: [PATCH 042/106] Allow users to fix glTF coordinate system imports (#19633) # Objective *Fixes #5670 as an opt-in for now* glTF uses the following coordinate system: - forward: Z - up: Y - right: -X and Bevy uses: - forward: -Z - up: Y - right: X For the longest time, Bevy has simply ignored this distinction. That caused issues when working across programs, as most software respects the glTF coordinate system when importing and exporting glTFs. Your scene might have looked correct in Blender, Maya, TrenchBroom, etc. but everything would be flipped when importing it into Bevy! ## Solution Add an option to the glTF loader to perform coordinate conversion. Note that this makes a distinction in the camera nodes, as glTF uses a different coordinate system for them. ## Follow Ups - Add global glTF loader settings, similar to the image loader, so that users can make third-party crates also load their glTFs with corrected coordinates - Decide on a migration strategy to make this the future default - Create an issue - Get feedback from Patrick Walton and Cart (not pinging them here to not spam them) - Include this pic for reference of how Blender assumes -Y as forward: ![image](https://github.com/user-attachments/assets/8f5ae364-48f0-46e4-922b-50bccb8d58b3) ## Testing I ran all glTF animation examples with the new setting enabled to validate that they look the same, just flipped. Also got a nice test scene from Chris that includes a camera inside the glTF. Thanks @ChristopherBiscardi! Blender (-Y forward): ![image](https://github.com/user-attachments/assets/129013f1-a025-488a-8764-c7ee5e7019a1) Bevy (-Z forward, but the model looks the wrong way): ![image](https://github.com/user-attachments/assets/842e00e0-48ce-4ca7-a88e-ea458ecbf852) Bevy with `convert_coordinates` enabled (-Z forward): ![image](https://github.com/user-attachments/assets/e97f3797-75a0-4d2b-ac54-130ba69f0a3c) Validation that the axes are correct with F3D's glTF viewer (+Z forward): ![image](https://github.com/user-attachments/assets/b9f02adf-a7b0-4a18-821f-fdd04426d3bd) --- crates/bevy_gltf/src/convert_coordinates.rs | 80 +++++++++++++++++++ crates/bevy_gltf/src/lib.rs | 1 + crates/bevy_gltf/src/loader/gltf_ext/scene.rs | 18 ++++- crates/bevy_gltf/src/loader/mod.rs | 55 +++++++++++-- crates/bevy_gltf/src/vertex_attributes.rs | 71 +++++++++++----- .../release-notes/convert-coordinates.md | 50 ++++++++++++ 6 files changed, 245 insertions(+), 30 deletions(-) create mode 100644 crates/bevy_gltf/src/convert_coordinates.rs create mode 100644 release-content/release-notes/convert-coordinates.md diff --git a/crates/bevy_gltf/src/convert_coordinates.rs b/crates/bevy_gltf/src/convert_coordinates.rs new file mode 100644 index 0000000000..4148cecd9a --- /dev/null +++ b/crates/bevy_gltf/src/convert_coordinates.rs @@ -0,0 +1,80 @@ +use core::f32::consts::PI; + +use bevy_math::{Mat4, Quat, Vec3}; +use bevy_transform::components::Transform; + +pub(crate) trait ConvertCoordinates { + /// Converts the glTF coordinates to Bevy's coordinate system. + /// - glTF: + /// - forward: Z + /// - up: Y + /// - right: -X + /// - Bevy: + /// - forward: -Z + /// - up: Y + /// - right: X + /// + /// See + fn convert_coordinates(self) -> Self; +} + +pub(crate) trait ConvertCameraCoordinates { + /// Like `convert_coordinates`, but uses the following for the lens rotation: + /// - forward: -Z + /// - up: Y + /// - right: X + /// + /// See + fn convert_camera_coordinates(self) -> Self; +} + +impl ConvertCoordinates for Vec3 { + fn convert_coordinates(self) -> Self { + Vec3::new(-self.x, self.y, -self.z) + } +} + +impl ConvertCoordinates for [f32; 3] { + fn convert_coordinates(self) -> Self { + [-self[0], self[1], -self[2]] + } +} + +impl ConvertCoordinates for [f32; 4] { + fn convert_coordinates(self) -> Self { + // Solution of q' = r q r* + [-self[0], self[1], -self[2], self[3]] + } +} + +impl ConvertCoordinates for Quat { + fn convert_coordinates(self) -> Self { + // Solution of q' = r q r* + Quat::from_array([-self.x, self.y, -self.z, self.w]) + } +} + +impl ConvertCoordinates for Mat4 { + fn convert_coordinates(self) -> Self { + let m: Mat4 = Mat4::from_scale(Vec3::new(-1.0, 1.0, -1.0)); + // Same as the original matrix + let m_inv = m; + m_inv * self * m + } +} + +impl ConvertCoordinates for Transform { + fn convert_coordinates(mut self) -> Self { + self.translation = self.translation.convert_coordinates(); + self.rotation = self.rotation.convert_coordinates(); + self + } +} + +impl ConvertCameraCoordinates for Transform { + fn convert_camera_coordinates(mut self) -> Self { + self.translation = self.translation.convert_coordinates(); + self.rotate_y(PI); + self + } +} diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index 97600f3ed0..4262d43eb7 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -91,6 +91,7 @@ //! You can use [`GltfAssetLabel`] to ensure you are using the correct label. mod assets; +mod convert_coordinates; mod label; mod loader; mod vertex_attributes; diff --git a/crates/bevy_gltf/src/loader/gltf_ext/scene.rs b/crates/bevy_gltf/src/loader/gltf_ext/scene.rs index 83e6778b99..3fce51d527 100644 --- a/crates/bevy_gltf/src/loader/gltf_ext/scene.rs +++ b/crates/bevy_gltf/src/loader/gltf_ext/scene.rs @@ -10,7 +10,10 @@ use itertools::Itertools; #[cfg(feature = "bevy_animation")] use bevy_platform::collections::{HashMap, HashSet}; -use crate::GltfError; +use crate::{ + convert_coordinates::{ConvertCameraCoordinates as _, ConvertCoordinates as _}, + GltfError, +}; pub(crate) fn node_name(node: &Node) -> Name { let name = node @@ -26,8 +29,8 @@ pub(crate) fn node_name(node: &Node) -> Name { /// on [`Node::transform()`](gltf::Node::transform) directly because it uses optimized glam types and /// if `libm` feature of `bevy_math` crate is enabled also handles cross /// platform determinism properly. -pub(crate) fn node_transform(node: &Node) -> Transform { - match node.transform() { +pub(crate) fn node_transform(node: &Node, convert_coordinates: bool) -> Transform { + let transform = match node.transform() { gltf::scene::Transform::Matrix { matrix } => { Transform::from_matrix(Mat4::from_cols_array_2d(&matrix)) } @@ -40,6 +43,15 @@ pub(crate) fn node_transform(node: &Node) -> Transform { rotation: bevy_math::Quat::from_array(rotation), scale: Vec3::from(scale), }, + }; + if convert_coordinates { + if node.camera().is_some() { + transform.convert_camera_coordinates() + } else { + transform.convert_coordinates() + } + } else { + transform } } diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index abc555002a..5e0f752e50 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -84,6 +84,7 @@ use self::{ texture::{texture_handle, texture_sampler, texture_transform_to_affine2}, }, }; +use crate::convert_coordinates::ConvertCoordinates as _; /// An error that occurs when loading a glTF file. #[derive(Error, Debug)] @@ -191,6 +192,16 @@ pub struct GltfLoaderSettings { pub default_sampler: Option, /// If true, the loader will ignore sampler data from gltf and use the default sampler. pub override_sampler: bool, + /// If true, the loader will convert glTF coordinates to Bevy's coordinate system. + /// - glTF: + /// - forward: Z + /// - up: Y + /// - right: -X + /// - Bevy: + /// - forward: -Z + /// - up: Y + /// - right: X + pub convert_coordinates: bool, } impl Default for GltfLoaderSettings { @@ -203,6 +214,7 @@ impl Default for GltfLoaderSettings { include_source: false, default_sampler: None, override_sampler: false, + convert_coordinates: false, } } } @@ -303,7 +315,16 @@ async fn load_gltf<'a, 'b, 'c>( match outputs { ReadOutputs::Translations(tr) => { let translation_property = animated_field!(Transform::translation); - let translations: Vec = tr.map(Vec3::from).collect(); + let translations: Vec = tr + .map(Vec3::from) + .map(|verts| { + if settings.convert_coordinates { + Vec3::convert_coordinates(verts) + } else { + verts + } + }) + .collect(); if keyframe_timestamps.len() == 1 { Some(VariableCurve::new(AnimatableCurve::new( translation_property, @@ -350,8 +371,17 @@ async fn load_gltf<'a, 'b, 'c>( } ReadOutputs::Rotations(rots) => { let rotation_property = animated_field!(Transform::rotation); - let rotations: Vec = - rots.into_f32().map(Quat::from_array).collect(); + let rotations: Vec = rots + .into_f32() + .map(Quat::from_array) + .map(|quat| { + if settings.convert_coordinates { + Quat::convert_coordinates(quat) + } else { + quat + } + }) + .collect(); if keyframe_timestamps.len() == 1 { Some(VariableCurve::new(AnimatableCurve::new( rotation_property, @@ -633,6 +663,7 @@ async fn load_gltf<'a, 'b, 'c>( accessor, &buffer_data, &loader.custom_vertex_attributes, + settings.convert_coordinates, ) { Ok((attribute, values)) => mesh.insert_attribute(attribute, values), Err(err) => warn!("{}", err), @@ -752,7 +783,17 @@ async fn load_gltf<'a, 'b, 'c>( let reader = gltf_skin.reader(|buffer| Some(&buffer_data[buffer.index()])); let local_to_bone_bind_matrices: Vec = reader .read_inverse_bind_matrices() - .map(|mats| mats.map(|mat| Mat4::from_cols_array_2d(&mat)).collect()) + .map(|mats| { + mats.map(|mat| Mat4::from_cols_array_2d(&mat)) + .map(|mat| { + if settings.convert_coordinates { + mat.convert_coordinates() + } else { + mat + } + }) + .collect() + }) .unwrap_or_else(|| { core::iter::repeat_n(Mat4::IDENTITY, gltf_skin.joints().len()).collect() }); @@ -834,7 +875,7 @@ async fn load_gltf<'a, 'b, 'c>( &node, children, mesh, - node_transform(&node), + node_transform(&node, settings.convert_coordinates), skin, node.extras().as_deref().map(GltfExtras::from), ); @@ -1306,7 +1347,7 @@ fn load_node( document: &Document, ) -> Result<(), GltfError> { let mut gltf_error = None; - let transform = node_transform(gltf_node); + let transform = node_transform(gltf_node, settings.convert_coordinates); let world_transform = *parent_transform * transform; // according to https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#instantiation, // if the determinant of the transform is negative we must invert the winding order of @@ -1359,7 +1400,6 @@ fn load_node( }, ..OrthographicProjection::default_3d() }; - Projection::Orthographic(orthographic_projection) } gltf::camera::Projection::Perspective(perspective) => { @@ -1377,6 +1417,7 @@ fn load_node( Projection::Perspective(perspective_projection) } }; + node.insert(( Camera3d::default(), projection, diff --git a/crates/bevy_gltf/src/vertex_attributes.rs b/crates/bevy_gltf/src/vertex_attributes.rs index d4ae811c90..2620d608a0 100644 --- a/crates/bevy_gltf/src/vertex_attributes.rs +++ b/crates/bevy_gltf/src/vertex_attributes.rs @@ -6,6 +6,8 @@ use gltf::{ }; use thiserror::Error; +use crate::convert_coordinates::ConvertCoordinates; + /// Represents whether integer data requires normalization #[derive(Copy, Clone)] struct Normalization(bool); @@ -132,15 +134,23 @@ impl<'a> VertexAttributeIter<'a> { } /// Materializes values for any supported format of vertex attribute - fn into_any_values(self) -> Result { + fn into_any_values(self, convert_coordinates: bool) -> Result { match self { VertexAttributeIter::F32(it) => Ok(Values::Float32(it.collect())), VertexAttributeIter::U32(it) => Ok(Values::Uint32(it.collect())), VertexAttributeIter::F32x2(it) => Ok(Values::Float32x2(it.collect())), VertexAttributeIter::U32x2(it) => Ok(Values::Uint32x2(it.collect())), - VertexAttributeIter::F32x3(it) => Ok(Values::Float32x3(it.collect())), + VertexAttributeIter::F32x3(it) => Ok(if convert_coordinates { + Values::Float32x3(it.map(ConvertCoordinates::convert_coordinates).collect()) + } else { + Values::Float32x3(it.collect()) + }), VertexAttributeIter::U32x3(it) => Ok(Values::Uint32x3(it.collect())), - VertexAttributeIter::F32x4(it) => Ok(Values::Float32x4(it.collect())), + VertexAttributeIter::F32x4(it) => Ok(if convert_coordinates { + Values::Float32x4(it.map(ConvertCoordinates::convert_coordinates).collect()) + } else { + Values::Float32x4(it.collect()) + }), VertexAttributeIter::U32x4(it) => Ok(Values::Uint32x4(it.collect())), VertexAttributeIter::S16x2(it, n) => { Ok(n.apply_either(it.collect(), Values::Snorm16x2, Values::Sint16x2)) @@ -188,7 +198,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U16x4(it, Normalization(true)) => Ok(Values::Float32x4( ReadColors::RgbaU16(it).into_rgba_f32().collect(), )), - s => s.into_any_values(), + s => s.into_any_values(false), } } @@ -198,7 +208,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U8x4(it, Normalization(false)) => { Ok(Values::Uint16x4(ReadJoints::U8(it).into_u16().collect())) } - s => s.into_any_values(), + s => s.into_any_values(false), } } @@ -211,7 +221,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U16x4(it, Normalization(true)) => { Ok(Values::Float32x4(ReadWeights::U16(it).into_f32().collect())) } - s => s.into_any_values(), + s => s.into_any_values(false), } } @@ -224,7 +234,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U16x2(it, Normalization(true)) => Ok(Values::Float32x2( ReadTexCoords::U16(it).into_f32().collect(), )), - s => s.into_any_values(), + s => s.into_any_values(false), } } } @@ -252,28 +262,49 @@ pub(crate) fn convert_attribute( accessor: gltf::Accessor, buffer_data: &Vec>, custom_vertex_attributes: &HashMap, MeshVertexAttribute>, + convert_coordinates: bool, ) -> Result<(MeshVertexAttribute, Values), ConvertAttributeError> { - if let Some((attribute, conversion)) = match &semantic { - gltf::Semantic::Positions => Some((Mesh::ATTRIBUTE_POSITION, ConversionMode::Any)), - gltf::Semantic::Normals => Some((Mesh::ATTRIBUTE_NORMAL, ConversionMode::Any)), - gltf::Semantic::Tangents => Some((Mesh::ATTRIBUTE_TANGENT, ConversionMode::Any)), - gltf::Semantic::Colors(0) => Some((Mesh::ATTRIBUTE_COLOR, ConversionMode::Rgba)), - gltf::Semantic::TexCoords(0) => Some((Mesh::ATTRIBUTE_UV_0, ConversionMode::TexCoord)), - gltf::Semantic::TexCoords(1) => Some((Mesh::ATTRIBUTE_UV_1, ConversionMode::TexCoord)), - gltf::Semantic::Joints(0) => { - Some((Mesh::ATTRIBUTE_JOINT_INDEX, ConversionMode::JointIndex)) + if let Some((attribute, conversion, convert_coordinates)) = match &semantic { + gltf::Semantic::Positions => Some(( + Mesh::ATTRIBUTE_POSITION, + ConversionMode::Any, + convert_coordinates, + )), + gltf::Semantic::Normals => Some(( + Mesh::ATTRIBUTE_NORMAL, + ConversionMode::Any, + convert_coordinates, + )), + gltf::Semantic::Tangents => Some(( + Mesh::ATTRIBUTE_TANGENT, + ConversionMode::Any, + convert_coordinates, + )), + gltf::Semantic::Colors(0) => Some((Mesh::ATTRIBUTE_COLOR, ConversionMode::Rgba, false)), + gltf::Semantic::TexCoords(0) => { + Some((Mesh::ATTRIBUTE_UV_0, ConversionMode::TexCoord, false)) } - gltf::Semantic::Weights(0) => { - Some((Mesh::ATTRIBUTE_JOINT_WEIGHT, ConversionMode::JointWeight)) + gltf::Semantic::TexCoords(1) => { + Some((Mesh::ATTRIBUTE_UV_1, ConversionMode::TexCoord, false)) } + gltf::Semantic::Joints(0) => Some(( + Mesh::ATTRIBUTE_JOINT_INDEX, + ConversionMode::JointIndex, + false, + )), + gltf::Semantic::Weights(0) => Some(( + Mesh::ATTRIBUTE_JOINT_WEIGHT, + ConversionMode::JointWeight, + false, + )), gltf::Semantic::Extras(name) => custom_vertex_attributes .get(name.as_str()) - .map(|attr| (*attr, ConversionMode::Any)), + .map(|attr| (*attr, ConversionMode::Any, false)), _ => None, } { let raw_iter = VertexAttributeIter::from_accessor(accessor.clone(), buffer_data); let converted_values = raw_iter.and_then(|iter| match conversion { - ConversionMode::Any => iter.into_any_values(), + ConversionMode::Any => iter.into_any_values(convert_coordinates), ConversionMode::Rgba => iter.into_rgba_values(), ConversionMode::TexCoord => iter.into_tex_coord_values(), ConversionMode::JointIndex => iter.into_joint_index_values(), diff --git a/release-content/release-notes/convert-coordinates.md b/release-content/release-notes/convert-coordinates.md new file mode 100644 index 0000000000..22e69fd4ef --- /dev/null +++ b/release-content/release-notes/convert-coordinates.md @@ -0,0 +1,50 @@ +--- +title: Allow importing glTFs with a corrected coordinate system +authors: ["@janhohenheim"] +pull_requests: [19633] +--- + +glTF uses the following coordinate system: + +- forward: Z +- up: Y +- right: -X + +and Bevy uses: + +- forward: -Z +- up: Y +- right: X + +This means that to correctly import glTFs into Bevy, vertex data should be rotated by 180 degrees around the Y axis. +For the longest time, Bevy has simply ignored this distinction. That caused issues when working across programs, as most software respects the +glTF coordinate system when importing and exporting glTFs. Your scene might have looked correct in Blender, Maya, TrenchBroom, etc. but everything would be flipped when importing it into Bevy! + +Long-term, we'd like to fix our glTF imports to use the correct coordinate system by default. +But changing the import behavior would mean that *all* imported glTFs of *all* users would suddenly look different, breaking their scenes! +Not to mention that any bugs in the conversion code would be incredibly frustating for users. + +This is why we are now gradually rolling out support for corrected glTF imports. Starting now you can opt into the new behavior by setting the `GltfLoaderSettings`: + +```rust +// old behavior, ignores glTF's coordinate system +let handle = asset_server.load("fox.gltf#Scene0"); + +// new behavior, converts glTF's coordinate system into Bevy's coordinate system +let handle = asset_server.load_with_settings( + "fox.gltf#Scene0", + |settings: &mut GltfLoaderSettings| { + settings.convert_coordinates = true; + }, +); +``` + +Afterwards, your scene will be oriented such that your modeling software's forward direction correctly corresponds to Bevy's forward direction. + +For example, Blender assumes -Y to be forward, so exporting the following model to glTF and loading it in Bevy with the new settings will ensure everything is +oriented the right way across all programs in your pipeline: + + +![Blender Coordinate System](blender-coords.png) + +If you opt into this, please let us know how it's working out! Is your scene looking like you expected? Are the animations playing correctly? Is the camera at the right place? Are the lights shining from the right spots? From 0c63aa2405c1e30fa17eb8702d7fa03f1dabb306 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 16 Jun 2025 22:55:32 +0100 Subject: [PATCH 043/106] Document `TextShadow` not supported by `Text2d` (#19533) # Objective Document `TextShadow` not supported by `Text2d` Co-authored-by: Alice Cecile --- crates/bevy_ui/src/ui_node.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 1c5ed364b9..d81f80f8b9 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -2875,6 +2875,8 @@ impl ComputedNodeTarget { } /// Adds a shadow behind text +/// +/// Not supported by `Text2d` #[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] #[reflect(Component, Default, Debug, Clone, PartialEq)] pub struct TextShadow { From 7237c2173bce13ff37024cdb5a29306a07538264 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Mon, 16 Jun 2025 15:00:33 -0700 Subject: [PATCH 044/106] Make observers metadata / storage publicly visible (#19608) # Objective Our strategy for storing observers is made up of several moving parts, which are ultimately fairly simple nested HashMaps. These types are currently `pub`, but lack any meaningful way to access this data. We have three options here: 1. Make these internals not `pub` at all. 2. Make the data read-only accessible. 3. Make the data mutably accessible. ## Solution I've opted for option 2, exposing read-only values. This is consistent with our existing approach to the ECS internals, allowing for easier debugging without risking wanton data corruption. If one day you would like to mutably access this data, please open an issue clearly explaining what you're trying to do. This was a pretty mechanical change, exposing fields via getters. I've also opted to do my best to clarify some field names and documentation: please double-check those for correctness. It was hard to be fully confident, as the field names and documentation was not very clear ;) ## Testing I spent some time going through the code paths, making sure that users can trace all the way from `World` to the leaf nodes. Reviewers, please ensure the same! ## Notes for reviewers This is part of a broader observer overhaul: I fully expect us to change up these internals and break these shiny new APIs. Probably even within the same cycle! But clean up your work area first: this sort of read-only getter and improved docs will be important to replace as we work. --- .../bevy_ecs/src/observer/entity_observer.rs | 8 +- crates/bevy_ecs/src/observer/mod.rs | 105 +++++++++++++----- crates/bevy_ecs/src/world/mod.rs | 6 + 3 files changed, 90 insertions(+), 29 deletions(-) diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_observer.rs index 5530b1f0fc..23be0e9672 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_observer.rs @@ -97,7 +97,7 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo let event_types = observer_state.descriptor.events.clone(); let components = observer_state.descriptor.components.clone(); for event_type in event_types { - let observers = world.observers.get_observers(event_type); + let observers = world.observers.get_observers_mut(event_type); if components.is_empty() { if let Some(map) = observers.entity_observers.get(&source).cloned() { observers.entity_observers.insert(target, map); @@ -108,8 +108,10 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo else { continue; }; - if let Some(map) = observers.entity_map.get(&source).cloned() { - observers.entity_map.insert(target, map); + if let Some(map) = + observers.entity_component_observers.get(&source).cloned() + { + observers.entity_component_observers.insert(target, map); } } } diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 4db65b888d..b3a8b3b5cd 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -515,40 +515,72 @@ impl ObserverTrigger { } } -// Map between an observer entity and its runner -type ObserverMap = EntityHashMap; +/// Map between an observer entity and its [`ObserverRunner`] +pub type ObserverMap = EntityHashMap; -/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger targeted at a specific component. +/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event targeted at a specific component. /// /// This is stored inside of [`CachedObservers`]. #[derive(Default, Debug)] pub struct CachedComponentObservers { - // Observers listening to triggers targeting this component - map: ObserverMap, - // Observers listening to triggers targeting this component on a specific entity - entity_map: EntityHashMap, + // Observers listening to events targeting this component, but not a specific entity + global_observers: ObserverMap, + // Observers listening to events targeting this component on a specific entity + entity_component_observers: EntityHashMap, } -/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger. +impl CachedComponentObservers { + /// Returns the observers listening for this trigger, regardless of target. + /// These observers will also respond to events targeting specific entities. + pub fn global_observers(&self) -> &ObserverMap { + &self.global_observers + } + + /// Returns the observers listening for this trigger targeting this component on a specific entity. + pub fn entity_component_observers(&self) -> &EntityHashMap { + &self.entity_component_observers + } +} + +/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event. /// /// This is stored inside of [`Observers`], specialized for each kind of observer. #[derive(Default, Debug)] pub struct CachedObservers { - // Observers listening for any time this trigger is fired - map: ObserverMap, + // Observers listening for any time this event is fired, regardless of target + // This will also respond to events targeting specific components or entities + global_observers: ObserverMap, // Observers listening for this trigger fired at a specific component component_observers: HashMap, // Observers listening for this trigger fired at a specific entity entity_observers: EntityHashMap, } +impl CachedObservers { + /// Returns the observers listening for this trigger, regardless of target. + /// These observers will also respond to events targeting specific components or entities. + pub fn global_observers(&self) -> &ObserverMap { + &self.global_observers + } + + /// Returns the observers listening for this trigger targeting components. + pub fn get_component_observers(&self) -> &HashMap { + &self.component_observers + } + + /// Returns the observers listening for this trigger targeting entities. + pub fn entity_observers(&self) -> &HashMap { + &self.component_observers + } +} + /// An internal lookup table tracking all of the observers in the world. /// /// Stores a cache mapping trigger ids to the registered observers. /// Some observer kinds (like [lifecycle](crate::lifecycle) observers) have a dedicated field, /// saving lookups for the most common triggers. /// -/// This is stored as a field of the [`World`]. +/// This can be accessed via [`World::observers`]. #[derive(Default, Debug)] pub struct Observers { // Cached ECS observers to save a lookup most common triggers. @@ -562,7 +594,7 @@ pub struct Observers { } impl Observers { - pub(crate) fn get_observers(&mut self, event_type: ComponentId) -> &mut CachedObservers { + pub(crate) fn get_observers_mut(&mut self, event_type: ComponentId) -> &mut CachedObservers { use crate::lifecycle::*; match event_type { @@ -575,7 +607,11 @@ impl Observers { } } - pub(crate) fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { + /// Attempts to get the observers for the given `event_type`. + /// + /// When accessing the observers for lifecycle events, such as [`Add`], [`Insert`], [`Replace`], [`Remove`], and [`Despawn`], + /// use the [`ComponentId`] constants from the [`lifecycle`](crate::lifecycle) module. + pub fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { use crate::lifecycle::*; match event_type { @@ -630,7 +666,10 @@ impl Observers { ); }; // Trigger observers listening for any kind of this trigger - observers.map.iter().for_each(&mut trigger_observer); + observers + .global_observers + .iter() + .for_each(&mut trigger_observer); // Trigger entity observers listening for this kind of trigger if let Some(target_entity) = current_target { @@ -643,12 +682,15 @@ impl Observers { trigger_for_components.for_each(|id| { if let Some(component_observers) = observers.component_observers.get(&id) { component_observers - .map + .global_observers .iter() .for_each(&mut trigger_observer); if let Some(target_entity) = current_target { - if let Some(map) = component_observers.entity_map.get(&target_entity) { + if let Some(map) = component_observers + .entity_component_observers + .get(&target_entity) + { map.iter().for_each(&mut trigger_observer); } } @@ -926,10 +968,12 @@ impl World { let descriptor = &observer_state.descriptor; for &event_type in &descriptor.events { - let cache = observers.get_observers(event_type); + let cache = observers.get_observers_mut(event_type); if descriptor.components.is_empty() && descriptor.entities.is_empty() { - cache.map.insert(observer_entity, observer_state.runner); + cache + .global_observers + .insert(observer_entity, observer_state.runner); } else if descriptor.components.is_empty() { // Observer is not targeting any components so register it as an entity observer for &watched_entity in &observer_state.descriptor.entities { @@ -951,11 +995,16 @@ impl World { }); if descriptor.entities.is_empty() { // Register for all triggers targeting the component - observers.map.insert(observer_entity, observer_state.runner); + observers + .global_observers + .insert(observer_entity, observer_state.runner); } else { // Register for each watched entity for &watched_entity in &descriptor.entities { - let map = observers.entity_map.entry(watched_entity).or_default(); + let map = observers + .entity_component_observers + .entry(watched_entity) + .or_default(); map.insert(observer_entity, observer_state.runner); } } @@ -970,9 +1019,9 @@ impl World { let observers = &mut self.observers; for &event_type in &descriptor.events { - let cache = observers.get_observers(event_type); + let cache = observers.get_observers_mut(event_type); if descriptor.components.is_empty() && descriptor.entities.is_empty() { - cache.map.remove(&entity); + cache.global_observers.remove(&entity); } else if descriptor.components.is_empty() { for watched_entity in &descriptor.entities { // This check should be unnecessary since this observer hasn't been unregistered yet @@ -990,20 +1039,24 @@ impl World { continue; }; if descriptor.entities.is_empty() { - observers.map.remove(&entity); + observers.global_observers.remove(&entity); } else { for watched_entity in &descriptor.entities { - let Some(map) = observers.entity_map.get_mut(watched_entity) else { + let Some(map) = + observers.entity_component_observers.get_mut(watched_entity) + else { continue; }; map.remove(&entity); if map.is_empty() { - observers.entity_map.remove(watched_entity); + observers.entity_component_observers.remove(watched_entity); } } } - if observers.map.is_empty() && observers.entity_map.is_empty() { + if observers.global_observers.is_empty() + && observers.entity_component_observers.is_empty() + { cache.component_observers.remove(component); if let Some(flag) = Observers::is_archetype_cached(event_type) { if let Some(by_component) = archetypes.by_component.get(component) { diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index ed7c1f2cdd..95accb20b5 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -261,6 +261,12 @@ impl World { &self.removed_components } + /// Retrieves this world's [`Observers`] list + #[inline] + pub fn observers(&self) -> &Observers { + &self.observers + } + /// Creates a new [`Commands`] instance that writes to the world's command queue /// Use [`World::flush`] to apply all queued commands #[inline] From 7c2289c96ebfd9c43bdca31aeb7136dde6a91769 Mon Sep 17 00:00:00 2001 From: atlv Date: Mon, 16 Jun 2025 18:02:30 -0400 Subject: [PATCH 045/106] Clarify GlobalTransform::to_isometry doc (#19645) # Objective - to_isometry is not a direct conversion, it involves computation. the docs could be clearer ## Solution - Improve docs ## Testing - its docs --- crates/bevy_transform/src/components/global_transform.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/bevy_transform/src/components/global_transform.rs b/crates/bevy_transform/src/components/global_transform.rs index b10d5a9d1a..d9fcf80737 100644 --- a/crates/bevy_transform/src/components/global_transform.rs +++ b/crates/bevy_transform/src/components/global_transform.rs @@ -139,8 +139,9 @@ impl GlobalTransform { } } - /// Returns the isometric part of the transformation as an [isometry]. Any scaling done by the - /// transformation will be ignored. + /// Computes a Scale-Rotation-Translation decomposition of the transformation and returns + /// the isometric part as an [isometry]. Any scaling done by the transformation will be ignored. + /// Note: this is a somewhat costly and lossy conversion. /// /// The transform is expected to be non-degenerate and without shearing, or the output /// will be invalid. From 209866cc27d5627d2e937b971a1a316d18bcf2b1 Mon Sep 17 00:00:00 2001 From: HippoGamus Date: Tue, 17 Jun 2025 00:05:19 +0200 Subject: [PATCH 046/106] Fix Keyboard observer not updating SliderValue (#19661) # Objective When the `CoreSlider`s `on_change` is set to None, Keyboard input, like ArrowKeys, does not update the `SliderValue`. ## Solution Handle the missing case, like it is done for Pointer. ## Testing - Did you test these changes? Yes: core_widgets & core_widgets_observers in both examples one has to remove / comment out the setting of `CoreSlider::on_change` to test the case of `on_change` being none. - Are there any parts that need more testing? No not that I am aware of. - How can other people (reviewers) test your changes? Is there anything specific they need to know? Yes: core_widgets & core_widgets_observers in both examples one has to remove / comment out the setting of `CoreSlider::on_change` to test the case of `on_change` being none. - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? I tested on linux + wayland. But it is unlikely that it would effect outcomes. --------- Co-authored-by: Alice Cecile --- crates/bevy_core_widgets/src/core_slider.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index 63a606be78..d7ab387852 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -394,6 +394,10 @@ fn slider_on_key_input( 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)); } } } From c0fa10b0c3a3cb1dbc565573b1d7601884798e49 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 16 Jun 2025 23:07:54 +0100 Subject: [PATCH 047/106] slider widget track click position `UiScale` fix (#19676) # Objective The position for track clicks in `core_slider` is calculated incorrectly when using `UiScale`. ## Solution `trigger.event().pointer_location.position` uses logical window coordinates, that is: `position = physical_position / window_scale_factor` while `ComputedNodeTarget::scale_factor` returns the window scale factor multiplied by Ui Scale: `target_scale_factor = window_scale_factor * ui_scale` So to get the physical position need to divide by the `UiScale`: ``` position * target_scale_factor / ui_scale = (physical_postion / window_scale_factor) * (window_scale_factor * ui_scale) / ui_scale = physical_position ``` I thought this was fixed during the slider PR review, but must have got missed somewhere or lost in a merge. ## Testing Can test using the `core_widgets` example` with `.insert_resource(UiScale(2.))` added to the bevy app. --- crates/bevy_core_widgets/src/core_slider.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index d7ab387852..e07a61a9c2 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -211,6 +211,7 @@ pub(crate) fn slider_on_pointer_down( 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. @@ -255,7 +256,7 @@ pub(crate) fn slider_on_pointer_down( // Detect track click. let local_pos = transform.try_inverse().unwrap().transform_point2( - trigger.event().pointer_location.position * node_target.scale_factor(), + trigger.event().pointer_location.position * node_target.scale_factor() / ui_scale.0, ); let track_width = node.size().x - thumb_size; // Avoid division by zero From 20781371e8022421a2d544e217f266360b7c8e5e Mon Sep 17 00:00:00 2001 From: Erick Z <567737+eckz@users.noreply.github.com> Date: Tue, 17 Jun 2025 00:09:15 +0200 Subject: [PATCH 048/106] Register `ReflectSerialize` for &'static str (#19680) # Objective - When trying to serialize an structure that contains `&'static str` using only Reflection, I get the following error: ``` "type `&str` did not register the `ReflectSerialize` or `ReflectSerializeWithRegistry` type data. For certain types, this may need to be registered manually using `register_type_data` (stack: ... -> `core::option::Option<&str>` -> `&str`)") ``` ## Solution - Register `ReflectSerialize` for `&str` ## Testing - `cargo run -p ci`: OK --- crates/bevy_reflect/src/impls/core/primitives.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_reflect/src/impls/core/primitives.rs b/crates/bevy_reflect/src/impls/core/primitives.rs index 8bee33b4c8..3600f2ece5 100644 --- a/crates/bevy_reflect/src/impls/core/primitives.rs +++ b/crates/bevy_reflect/src/impls/core/primitives.rs @@ -282,6 +282,7 @@ impl GetTypeRegistration for &'static str { let mut registration = TypeRegistration::of::(); registration.insert::(FromType::::from_type()); registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); registration } } From 2915a3b90389cc46314878facc9cbfad7a14d177 Mon Sep 17 00:00:00 2001 From: atlv Date: Tue, 17 Jun 2025 14:37:26 -0400 Subject: [PATCH 049/106] rename GlobalTransform::compute_matrix to to_matrix (#19643) # Objective - compute_matrix doesn't compute anything, it just puts an Affine3A into a Mat4. the name is inaccurate ## Solution - rename it to conform with to_isometry (which, ironically, does compute a decomposition which is rather expensive) ## Testing - Its a rename. If it compiles, its good to go --------- Co-authored-by: Alice Cecile --- crates/bevy_pbr/src/atmosphere/resources.rs | 2 +- crates/bevy_pbr/src/cluster/assign.rs | 2 +- crates/bevy_pbr/src/light/mod.rs | 4 ++-- crates/bevy_pbr/src/light_probe/mod.rs | 2 +- crates/bevy_pbr/src/prepass/mod.rs | 4 ++-- crates/bevy_pbr/src/volumetric_fog/render.rs | 4 ++-- .../src/mesh_picking/ray_cast/intersections.rs | 16 ++++++++-------- .../src/mesh_picking/ray_cast/mod.rs | 4 ++-- crates/bevy_render/src/camera/camera.rs | 8 +++----- crates/bevy_render/src/camera/projection.rs | 3 +-- crates/bevy_render/src/view/mod.rs | 4 ++-- crates/bevy_solari/src/scene/binder.rs | 2 +- .../src/components/global_transform.rs | 2 +- .../rename_global_transform_compute_matrix.md | 6 ++++++ 14 files changed, 33 insertions(+), 30 deletions(-) create mode 100644 release-content/migration-guides/rename_global_transform_compute_matrix.md diff --git a/crates/bevy_pbr/src/atmosphere/resources.rs b/crates/bevy_pbr/src/atmosphere/resources.rs index 8c57fb8eb9..d7c93c4418 100644 --- a/crates/bevy_pbr/src/atmosphere/resources.rs +++ b/crates/bevy_pbr/src/atmosphere/resources.rs @@ -560,7 +560,7 @@ pub(super) fn prepare_atmosphere_transforms( }; for (entity, view) in &views { - let world_from_view = view.world_from_view.compute_matrix(); + let world_from_view = view.world_from_view.to_matrix(); let camera_z = world_from_view.z_axis.truncate(); let camera_y = world_from_view.y_axis.truncate(); let atmo_z = camera_z diff --git a/crates/bevy_pbr/src/cluster/assign.rs b/crates/bevy_pbr/src/cluster/assign.rs index 1b7b3563d7..b0c0fb6347 100644 --- a/crates/bevy_pbr/src/cluster/assign.rs +++ b/crates/bevy_pbr/src/cluster/assign.rs @@ -353,7 +353,7 @@ pub(crate) fn assign_objects_to_clusters( let mut requested_cluster_dimensions = config.dimensions_for_screen_size(screen_size); - let world_from_view = camera_transform.compute_matrix(); + let world_from_view = camera_transform.to_matrix(); let view_from_world_scale = camera_transform.compute_transform().scale.recip(); let view_from_world_scale_max = view_from_world_scale.abs().max_element(); let view_from_world = world_from_view.inverse(); diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 4ff4662bb2..1ff057b0a1 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -341,7 +341,7 @@ pub fn build_directional_light_cascades( .iter() .filter_map(|(entity, transform, projection, camera)| { if camera.is_active { - Some((entity, projection, transform.compute_matrix())) + Some((entity, projection, transform.to_matrix())) } else { None } @@ -357,7 +357,7 @@ pub fn build_directional_light_cascades( // light_to_world has orthogonal upper-left 3x3 and zero translation. // Even though only the direction (i.e. rotation) of the light matters, we don't constrain // users to not change any other aspects of the transform - there's no guarantee - // `transform.compute_matrix()` will give us a matrix with our desired properties. + // `transform.to_matrix()` will give us a matrix with our desired properties. // Instead, we directly create a good matrix from just the rotation. let world_from_light = Mat4::from_quat(transform.compute_transform().rotation); let light_to_world_inverse = world_from_light.inverse(); diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index 74710ce1d5..6f863273ef 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -595,7 +595,7 @@ where ) -> Option> { environment_map.id(image_assets).map(|id| LightProbeInfo { world_from_light: light_probe_transform.affine(), - light_from_world: light_probe_transform.compute_matrix().inverse(), + light_from_world: light_probe_transform.to_matrix().inverse(), asset_id: id, intensity: environment_map.intensity(), affects_lightmapped_mesh_diffuse: environment_map.affects_lightmapped_mesh_diffuse(), diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 1ae0c7fa84..aef2b74177 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -216,7 +216,7 @@ pub fn update_previous_view_data( query: Query<(Entity, &Camera, &GlobalTransform), Or<(With, With)>>, ) { for (entity, camera, camera_transform) in &query { - let world_from_view = camera_transform.compute_matrix(); + let world_from_view = camera_transform.to_matrix(); let view_from_world = world_from_view.inverse(); let view_from_clip = camera.clip_from_view().inverse(); @@ -703,7 +703,7 @@ pub fn prepare_previous_view_uniforms( let prev_view_data = match maybe_previous_view_uniforms { Some(previous_view) => previous_view.clone(), None => { - let world_from_view = camera.world_from_view.compute_matrix(); + let world_from_view = camera.world_from_view.to_matrix(); let view_from_world = world_from_view.inverse(); let view_from_clip = camera.clip_from_view.inverse(); diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index 625ff42dc5..cf2989a980 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -700,7 +700,7 @@ pub fn prepare_volumetric_fog_uniforms( // Do this up front to avoid O(n^2) matrix inversion. local_from_world_matrices.clear(); for (_, _, fog_transform) in fog_volumes.iter() { - local_from_world_matrices.push(fog_transform.compute_matrix().inverse()); + local_from_world_matrices.push(fog_transform.to_matrix().inverse()); } let uniform_count = view_targets.iter().len() * local_from_world_matrices.len(); @@ -712,7 +712,7 @@ pub fn prepare_volumetric_fog_uniforms( }; for (view_entity, extracted_view, volumetric_fog) in view_targets.iter() { - let world_from_view = extracted_view.world_from_view.compute_matrix(); + let world_from_view = extracted_view.world_from_view.to_matrix(); let mut view_fog_volumes = vec![]; diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs index 9988a96e19..5ac2d89887 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs @@ -317,7 +317,7 @@ mod tests { #[test] fn ray_mesh_intersection_simple() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = None; @@ -338,7 +338,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0, 1, 2]); @@ -359,7 +359,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]); @@ -381,7 +381,7 @@ mod tests { #[test] fn ray_mesh_intersection_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]); @@ -403,7 +403,7 @@ mod tests { #[test] fn ray_mesh_intersection_missing_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[]); let indices: Option<&[u16]> = None; @@ -424,7 +424,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices_missing_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[]); let indices: Option<&[u16]> = Some(&[0, 1, 2]); @@ -445,7 +445,7 @@ mod tests { #[test] fn ray_mesh_intersection_not_enough_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0]); @@ -466,7 +466,7 @@ mod tests { #[test] fn ray_mesh_intersection_bad_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0, 1, 3]); diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs index c1f465b96a..e42dc160e2 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs @@ -233,7 +233,7 @@ impl<'w, 's> MeshRayCast<'w, 's> { if let Some(distance) = ray_aabb_intersection_3d( ray, &Aabb3d::new(aabb.center, aabb.half_extents), - &transform.compute_matrix(), + &transform.to_matrix(), ) { aabb_hits_tx.send((FloatOrd(distance), entity)).ok(); } @@ -287,7 +287,7 @@ impl<'w, 's> MeshRayCast<'w, 's> { // Perform the actual ray cast. let _ray_cast_guard = ray_cast_guard.enter(); - let transform = transform.compute_matrix(); + let transform = transform.to_matrix(); let intersection = ray_intersection_over_mesh(mesh, &transform, ray, backfaces); if let Some(intersection) = intersection { diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index fd3b8cb4b2..2bddcd0d05 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -601,8 +601,7 @@ impl Camera { rect_relative.y = 1.0 - rect_relative.y; let ndc = rect_relative * 2. - Vec2::ONE; - let ndc_to_world = - camera_transform.compute_matrix() * self.computed.clip_from_view.inverse(); + let ndc_to_world = camera_transform.to_matrix() * self.computed.clip_from_view.inverse(); let world_near_plane = ndc_to_world.project_point3(ndc.extend(1.)); // Using EPSILON because an ndc with Z = 0 returns NaNs. let world_far_plane = ndc_to_world.project_point3(ndc.extend(f32::EPSILON)); @@ -668,7 +667,7 @@ impl Camera { ) -> Option { // Build a transformation matrix to convert from world space to NDC using camera data let clip_from_world: Mat4 = - self.computed.clip_from_view * camera_transform.compute_matrix().inverse(); + self.computed.clip_from_view * camera_transform.to_matrix().inverse(); let ndc_space_coords: Vec3 = clip_from_world.project_point3(world_position); (!ndc_space_coords.is_nan()).then_some(ndc_space_coords) @@ -689,8 +688,7 @@ impl Camera { /// Will panic if the projection matrix is invalid (has a determinant of 0) and `glam_assert` is enabled. pub fn ndc_to_world(&self, camera_transform: &GlobalTransform, ndc: Vec3) -> Option { // Build a transformation matrix to convert from NDC to world space using camera data - let ndc_to_world = - camera_transform.compute_matrix() * self.computed.clip_from_view.inverse(); + let ndc_to_world = camera_transform.to_matrix() * self.computed.clip_from_view.inverse(); let world_space_coords = ndc_to_world.project_point3(ndc); diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index a7796a1d1a..ee2a5080d2 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -93,8 +93,7 @@ pub trait CameraProjection { /// This code is called by [`update_frusta`](crate::view::visibility::update_frusta) system /// for each camera to update its frustum. fn compute_frustum(&self, camera_transform: &GlobalTransform) -> Frustum { - let clip_from_world = - self.get_clip_from_view() * camera_transform.compute_matrix().inverse(); + let clip_from_world = self.get_clip_from_view() * camera_transform.to_matrix().inverse(); Frustum::from_clip_from_world_custom_far( &clip_from_world, &camera_transform.translation(), diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index d25e8e7f49..b2b90d0b24 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -316,7 +316,7 @@ pub struct ExtractedView { impl ExtractedView { /// Creates a 3D rangefinder for a view pub fn rangefinder3d(&self) -> ViewRangefinder3d { - ViewRangefinder3d::from_world_from_view(&self.world_from_view.compute_matrix()) + ViewRangefinder3d::from_world_from_view(&self.world_from_view.to_matrix()) } } @@ -934,7 +934,7 @@ pub fn prepare_view_uniforms( } let view_from_clip = clip_from_view.inverse(); - let world_from_view = extracted_view.world_from_view.compute_matrix(); + let world_from_view = extracted_view.world_from_view.to_matrix(); let view_from_world = world_from_view.inverse(); let clip_from_world = if temporal_jitter.is_some() { diff --git a/crates/bevy_solari/src/scene/binder.rs b/crates/bevy_solari/src/scene/binder.rs index 59a8020566..889efb538e 100644 --- a/crates/bevy_solari/src/scene/binder.rs +++ b/crates/bevy_solari/src/scene/binder.rs @@ -143,7 +143,7 @@ pub fn prepare_raytracing_scene_bindings( continue; }; - let transform = transform.compute_matrix(); + let transform = transform.to_matrix(); *tlas.get_mut_single(instance_id).unwrap() = Some(TlasInstance::new( blas, tlas_transform(&transform), diff --git a/crates/bevy_transform/src/components/global_transform.rs b/crates/bevy_transform/src/components/global_transform.rs index d9fcf80737..cd7db6ef71 100644 --- a/crates/bevy_transform/src/components/global_transform.rs +++ b/crates/bevy_transform/src/components/global_transform.rs @@ -115,7 +115,7 @@ impl GlobalTransform { /// Returns the 3d affine transformation matrix as a [`Mat4`]. #[inline] - pub fn compute_matrix(&self) -> Mat4 { + pub fn to_matrix(&self) -> Mat4 { Mat4::from(self.0) } diff --git a/release-content/migration-guides/rename_global_transform_compute_matrix.md b/release-content/migration-guides/rename_global_transform_compute_matrix.md new file mode 100644 index 0000000000..d4676ecc5e --- /dev/null +++ b/release-content/migration-guides/rename_global_transform_compute_matrix.md @@ -0,0 +1,6 @@ +--- +title: GlobalTransform::compute_matrix rename +pull_requests: [19643] +--- + +`GlobalTransform::compute_matrix` has been renamed to `GlobalTransform::to_matrix` because it does not compute anything, it simply moves data into a different type. From d1c6fbea570be64f8e24e943ee5a3cef221794e0 Mon Sep 17 00:00:00 2001 From: Alejandro Pascual Date: Tue, 17 Jun 2025 21:48:37 +0200 Subject: [PATCH 050/106] Support fallible one-shot systems (#19678) Closes #19677. I don't think that the output type needs to be `Send`. I've done some test at it seems to work fine without it, which in IMO makes sense, but please correct me if that is not the case. --- .../bevy_ecs/src/system/commands/command.rs | 15 ++++-- crates/bevy_ecs/src/system/commands/mod.rs | 16 +++--- crates/bevy_ecs/src/system/system_registry.rs | 54 +++++++++++++++++-- 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs index 84a2fdf4e9..f3fc677e47 100644 --- a/crates/bevy_ecs/src/system/commands/command.rs +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -144,10 +144,11 @@ where /// A [`Command`] that runs the given system, /// caching its [`SystemId`] in a [`CachedSystemId`](crate::system::CachedSystemId) resource. -pub fn run_system_cached(system: S) -> impl Command +pub fn run_system_cached(system: S) -> impl Command where + O: 'static, M: 'static, - S: IntoSystem<(), (), M> + Send + 'static, + S: IntoSystem<(), O, M> + Send + 'static, { move |world: &mut World| -> Result { world.run_system_cached(system)?; @@ -157,11 +158,15 @@ where /// A [`Command`] that runs the given system with the given input value, /// caching its [`SystemId`] in a [`CachedSystemId`](crate::system::CachedSystemId) resource. -pub fn run_system_cached_with(system: S, input: I::Inner<'static>) -> impl Command +pub fn run_system_cached_with( + system: S, + input: I::Inner<'static>, +) -> impl Command where I: SystemInput: Send> + Send + 'static, + O: 'static, M: 'static, - S: IntoSystem + Send + 'static, + S: IntoSystem + Send + 'static, { move |world: &mut World| -> Result { world.run_system_cached_with(system, input)?; @@ -175,7 +180,7 @@ where pub fn unregister_system(system_id: SystemId) -> impl Command where I: SystemInput + Send + 'static, - O: Send + 'static, + O: 'static, { move |world: &mut World| -> Result { world.unregister_system(system_id)?; diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index d36588d377..84f9784228 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -872,7 +872,7 @@ impl<'w, 's> Commands<'w, 's> { /// /// It will internally return a [`RegisteredSystemError`](crate::system::system_registry::RegisteredSystemError), /// which will be handled by [logging the error at the `warn` level](warn). - pub fn run_system(&mut self, id: SystemId) { + pub fn run_system(&mut self, id: SystemId<(), O>) { self.queue(command::run_system(id).handle_error_with(warn)); } @@ -965,7 +965,7 @@ impl<'w, 's> Commands<'w, 's> { ) -> SystemId where I: SystemInput + Send + 'static, - O: Send + 'static, + O: 'static, { let entity = self.spawn_empty().id(); let system = RegisteredSystem::::new(Box::new(IntoSystem::into_system(system))); @@ -990,7 +990,7 @@ impl<'w, 's> Commands<'w, 's> { pub fn unregister_system(&mut self, system_id: SystemId) where I: SystemInput + Send + 'static, - O: Send + 'static, + O: 'static, { self.queue(command::unregister_system(system_id).handle_error_with(warn)); } @@ -1039,10 +1039,11 @@ impl<'w, 's> Commands<'w, 's> { /// consider passing them in as inputs via [`Commands::run_system_cached_with`]. /// /// If that's not an option, consider [`Commands::register_system`] instead. - pub fn run_system_cached(&mut self, system: S) + pub fn run_system_cached(&mut self, system: S) where + O: 'static, M: 'static, - S: IntoSystem<(), (), M> + Send + 'static, + S: IntoSystem<(), O, M> + Send + 'static, { self.queue(command::run_system_cached(system).handle_error_with(warn)); } @@ -1069,11 +1070,12 @@ impl<'w, 's> Commands<'w, 's> { /// consider passing them in as inputs. /// /// If that's not an option, consider [`Commands::register_system`] instead. - pub fn run_system_cached_with(&mut self, system: S, input: I::Inner<'static>) + pub fn run_system_cached_with(&mut self, system: S, input: I::Inner<'static>) where I: SystemInput: Send> + Send + 'static, + O: 'static, M: 'static, - S: IntoSystem + Send + 'static, + S: IntoSystem + Send + 'static, { self.queue(command::run_system_cached_with(system, input).handle_error_with(warn)); } diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index 272cc85d0d..6889a8f4cd 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -651,6 +651,19 @@ mod tests { assert_eq!(output, NonCopy(3)); } + #[test] + fn fallible_system() { + fn sys() -> Result<()> { + Err("error")?; + Ok(()) + } + + let mut world = World::new(); + let fallible_system_id = world.register_system(sys); + let output = world.run_system(fallible_system_id); + assert!(matches!(output, Ok(Err(_)))); + } + #[test] fn exclusive_system() { let mut world = World::new(); @@ -751,19 +764,54 @@ mod tests { assert!(matches!(output, Ok(x) if x == four())); } + #[test] + fn cached_fallible_system() { + fn sys() -> Result<()> { + Err("error")?; + Ok(()) + } + + let mut world = World::new(); + let fallible_system_id = world.register_system_cached(sys); + let output = world.run_system(fallible_system_id); + assert!(matches!(output, Ok(Err(_)))); + let output = world.run_system_cached(sys); + assert!(matches!(output, Ok(Err(_)))); + let output = world.run_system_cached_with(sys, ()); + assert!(matches!(output, Ok(Err(_)))); + } + #[test] fn cached_system_commands() { fn sys(mut counter: ResMut) { - counter.0 = 1; + counter.0 += 1; } let mut world = World::new(); world.insert_resource(Counter(0)); - world.commands().run_system_cached(sys); world.flush_commands(); - assert_eq!(world.resource::().0, 1); + world.commands().run_system_cached_with(sys, ()); + world.flush_commands(); + assert_eq!(world.resource::().0, 2); + } + + #[test] + fn cached_fallible_system_commands() { + fn sys(mut counter: ResMut) -> Result { + counter.0 += 1; + Ok(()) + } + + let mut world = World::new(); + world.insert_resource(Counter(0)); + world.commands().run_system_cached(sys); + world.flush_commands(); + assert_eq!(world.resource::().0, 1); + world.commands().run_system_cached_with(sys, ()); + world.flush_commands(); + assert_eq!(world.resource::().0, 2); } #[test] From a750cfe4a178d303efc3d8c14ac626fd69402df3 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Tue, 17 Jun 2025 22:20:13 +0200 Subject: [PATCH 051/106] Split CursorOptions off of Window (#19668) # Objective - Fixes #19627 - Tackles part of #19644 - Supersedes #19629 - `Window` has become a very very very big component - As such, our change detection does not *really* work on it, as e.g. moving the mouse will cause a change for the entire window - We circumvented this with a cache - But, some things *shouldn't* be cached as they can be changed from outside the user's control, notably the cursor grab mode on web - So, we need to disable the cache for that - But because change detection is broken, that would result in the cursor grab mode being set every frame the mouse is moved - That is usually *not* what a dev wants, as it forces the cursor to be locked even when the end-user is trying to free the cursor on the browser - the cache in this situation is invalid due to #8949 ## Solution - Split `Window` into multiple components, each with working change detection - Disable caching of the cursor grab mode - This will only attempt to force the grab mode when the `CursorOptions` were touched by the user, which is *much* rarer than simply moving the mouse. - If this PR is merged, I'll do the exact same for the other constituents of `Window` as a follow-up ## Testing - Ran all the changed examples --- crates/bevy_window/src/lib.rs | 17 +- crates/bevy_window/src/window.rs | 9 +- crates/bevy_winit/src/lib.rs | 6 +- crates/bevy_winit/src/state.rs | 11 +- crates/bevy_winit/src/system.rs | 162 +++++++++++------- crates/bevy_winit/src/winit_windows.rs | 15 +- examples/games/desk_toy.rs | 11 +- examples/helpers/camera_controller.rs | 16 +- examples/input/mouse_grab.rs | 15 +- examples/ui/window_fallthrough.rs | 6 +- examples/window/window_settings.rs | 10 +- .../migration-guides/split-window.md | 44 +++++ .../fixed-window-position.patch | 2 +- 13 files changed, 212 insertions(+), 112 deletions(-) create mode 100644 release-content/migration-guides/split-window.md diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index fb8f1fb18f..22e657cf03 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -57,6 +57,7 @@ impl Default for WindowPlugin { fn default() -> Self { WindowPlugin { primary_window: Some(Window::default()), + primary_cursor_options: Some(CursorOptions::default()), exit_condition: ExitCondition::OnAllClosed, close_when_requested: true, } @@ -76,6 +77,13 @@ pub struct WindowPlugin { /// [`exit_on_all_closed`]. pub primary_window: Option, + /// Settings for the cursor on the primary window. + /// + /// Defaults to `Some(CursorOptions::default())`. + /// + /// Has no effect if [`WindowPlugin::primary_window`] is `None`. + pub primary_cursor_options: Option, + /// Whether to exit the app when there are no open windows. /// /// If disabling this, ensure that you send the [`bevy_app::AppExit`] @@ -122,10 +130,14 @@ impl Plugin for WindowPlugin { .add_event::(); if let Some(primary_window) = &self.primary_window { - app.world_mut().spawn(primary_window.clone()).insert(( + let mut entity_commands = app.world_mut().spawn(primary_window.clone()); + entity_commands.insert(( PrimaryWindow, RawHandleWrapperHolder(Arc::new(Mutex::new(None))), )); + if let Some(primary_cursor_options) = &self.primary_cursor_options { + entity_commands.insert(primary_cursor_options.clone()); + } } match self.exit_condition { @@ -168,7 +180,8 @@ impl Plugin for WindowPlugin { // Register window descriptor and related types #[cfg(feature = "bevy_reflect")] app.register_type::() - .register_type::(); + .register_type::() + .register_type::(); } } diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 4f7d7b2f7b..e2a9ca3c0f 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -158,10 +158,8 @@ impl ContainsEntity for NormalizedWindowRef { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] +#[require(CursorOptions)] pub struct Window { - /// The cursor options of this window. Cursor icons are set with the `Cursor` component on the - /// window entity. - pub cursor_options: CursorOptions, /// What presentation mode to give the window. pub present_mode: PresentMode, /// Which fullscreen or windowing mode should be used. @@ -470,7 +468,6 @@ impl Default for Window { Self { title: DEFAULT_WINDOW_TITLE.to_owned(), name: None, - cursor_options: Default::default(), present_mode: Default::default(), mode: Default::default(), position: Default::default(), @@ -728,11 +725,11 @@ impl WindowResizeConstraints { } /// Cursor data for a [`Window`]. -#[derive(Debug, Clone)] +#[derive(Component, Debug, Clone)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), - reflect(Debug, Default, Clone) + reflect(Component, Debug, Default, Clone) )] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 6d814d4ac2..8926095dc0 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -25,8 +25,8 @@ use winit::{event_loop::EventLoop, window::WindowId}; use bevy_a11y::AccessibilityRequested; use bevy_app::{App, Last, Plugin}; use bevy_ecs::prelude::*; -use bevy_window::{exit_on_all_closed, Window, WindowCreated}; -use system::{changed_windows, check_keyboard_focus_lost, despawn_windows}; +use bevy_window::{exit_on_all_closed, CursorOptions, Window, WindowCreated}; +use system::{changed_cursor_options, changed_windows, check_keyboard_focus_lost, despawn_windows}; pub use system::{create_monitors, create_windows}; #[cfg(all(target_family = "wasm", target_os = "unknown"))] pub use winit::platform::web::CustomCursorExtWebSys; @@ -142,6 +142,7 @@ impl Plugin for WinitPlugin { // `exit_on_all_closed` only checks if windows exist but doesn't access data, // so we don't need to care about its ordering relative to `changed_windows` changed_windows.ambiguous_with(exit_on_all_closed), + changed_cursor_options, despawn_windows, check_keyboard_focus_lost, ) @@ -211,6 +212,7 @@ pub type CreateWindowParams<'w, 's, F = ()> = ( ( Entity, &'static mut Window, + &'static CursorOptions, Option<&'static RawHandleWrapperHolder>, ), F, diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index 934de5dad7..5b873d4620 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -46,7 +46,7 @@ use bevy_window::{ WindowScaleFactorChanged, WindowThemeChanged, }; #[cfg(target_os = "android")] -use bevy_window::{PrimaryWindow, RawHandleWrapper}; +use bevy_window::{CursorOptions, PrimaryWindow, RawHandleWrapper}; use crate::{ accessibility::ACCESS_KIT_ADAPTERS, @@ -474,7 +474,7 @@ impl ApplicationHandler for WinitAppRunnerState { if let Ok((window_component, mut cache)) = windows.get_mut(self.world_mut(), window) { if window_component.is_changed() { - cache.window = window_component.clone(); + **cache = window_component.clone(); } } }); @@ -605,10 +605,12 @@ impl WinitAppRunnerState { { // Get windows that are cached but without raw handles. Those window were already created, but got their // handle wrapper removed when the app was suspended. + let mut query = self.world_mut() - .query_filtered::<(Entity, &Window), (With, Without)>(); - if let Ok((entity, window)) = query.single(&self.world()) { + .query_filtered::<(Entity, &Window, &CursorOptions), (With, Without)>(); + if let Ok((entity, window, cursor_options)) = query.single(&self.world()) { let window = window.clone(); + let cursor_options = cursor_options.clone(); WINIT_WINDOWS.with_borrow_mut(|winit_windows| { ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { @@ -622,6 +624,7 @@ impl WinitAppRunnerState { event_loop, entity, &window, + &cursor_options, adapters, &mut handlers, &accessibility_requested, diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 873949ea89..3cc7c5a5f6 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ + change_detection::DetectChangesMut, entity::Entity, event::EventWriter, lifecycle::RemovedComponents, @@ -10,9 +12,9 @@ use bevy_ecs::{ }; use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput}; use bevy_window::{ - ClosingWindow, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, WindowClosed, - WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode, WindowResized, - WindowWrapper, + ClosingWindow, CursorOptions, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, + WindowClosed, WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode, + WindowResized, WindowWrapper, }; use tracing::{error, info, warn}; @@ -59,7 +61,7 @@ pub fn create_windows( ) { WINIT_WINDOWS.with_borrow_mut(|winit_windows| { ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { - for (entity, mut window, handle_holder) in &mut created_windows { + for (entity, mut window, cursor_options, handle_holder) in &mut created_windows { if winit_windows.get_window(entity).is_some() { continue; } @@ -70,6 +72,7 @@ pub fn create_windows( event_loop, entity, &window, + cursor_options, adapters, &mut handlers, &accessibility_requested, @@ -85,9 +88,8 @@ pub fn create_windows( .set_scale_factor_and_apply_to_physical_size(winit_window.scale_factor() as f32); commands.entity(entity).insert(( - CachedWindow { - window: window.clone(), - }, + CachedWindow(window.clone()), + CachedCursorOptions(cursor_options.clone()), WinitWindowPressedKeys::default(), )); @@ -281,10 +283,12 @@ pub(crate) fn despawn_windows( } /// The cached state of the window so we can check which properties were changed from within the app. -#[derive(Debug, Clone, Component)] -pub struct CachedWindow { - pub window: Window, -} +#[derive(Debug, Clone, Component, Deref, DerefMut)] +pub(crate) struct CachedWindow(Window); + +/// The cached state of the window so we can check which properties were changed from within the app. +#[derive(Debug, Clone, Component, Deref, DerefMut)] +pub(crate) struct CachedCursorOptions(CursorOptions); /// Propagates changes from [`Window`] entities to the [`winit`] backend. /// @@ -306,11 +310,11 @@ pub(crate) fn changed_windows( continue; }; - if window.title != cache.window.title { + if window.title != cache.title { winit_window.set_title(window.title.as_str()); } - if window.mode != cache.window.mode { + if window.mode != cache.mode { let new_mode = match window.mode { WindowMode::BorderlessFullscreen(monitor_selection) => { Some(Some(winit::window::Fullscreen::Borderless(select_monitor( @@ -352,15 +356,15 @@ pub(crate) fn changed_windows( } } - if window.resolution != cache.window.resolution { + if window.resolution != cache.resolution { let mut physical_size = PhysicalSize::new( window.resolution.physical_width(), window.resolution.physical_height(), ); let cached_physical_size = PhysicalSize::new( - cache.window.physical_width(), - cache.window.physical_height(), + cache.physical_width(), + cache.physical_height(), ); let base_scale_factor = window.resolution.base_scale_factor(); @@ -368,12 +372,12 @@ pub(crate) fn changed_windows( // Note: this may be different from `winit`'s base scale factor if // `scale_factor_override` is set to Some(f32) let scale_factor = window.scale_factor(); - let cached_scale_factor = cache.window.scale_factor(); + let cached_scale_factor = cache.scale_factor(); // Check and update `winit`'s physical size only if the window is not maximized if scale_factor != cached_scale_factor && !winit_window.is_maximized() { let logical_size = - if let Some(cached_factor) = cache.window.resolution.scale_factor_override() { + if let Some(cached_factor) = cache.resolution.scale_factor_override() { physical_size.to_logical::(cached_factor as f64) } else { physical_size.to_logical::(base_scale_factor as f64) @@ -397,7 +401,7 @@ pub(crate) fn changed_windows( } } - if window.physical_cursor_position() != cache.window.physical_cursor_position() { + if window.physical_cursor_position() != cache.physical_cursor_position() { if let Some(physical_position) = window.physical_cursor_position() { let position = PhysicalPosition::new(physical_position.x, physical_position.y); @@ -407,44 +411,23 @@ pub(crate) fn changed_windows( } } - if window.cursor_options.grab_mode != cache.window.cursor_options.grab_mode - && crate::winit_windows::attempt_grab(winit_window, window.cursor_options.grab_mode) - .is_err() - { - window.cursor_options.grab_mode = cache.window.cursor_options.grab_mode; - } - - if window.cursor_options.visible != cache.window.cursor_options.visible { - winit_window.set_cursor_visible(window.cursor_options.visible); - } - - if window.cursor_options.hit_test != cache.window.cursor_options.hit_test { - if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) { - window.cursor_options.hit_test = cache.window.cursor_options.hit_test; - warn!( - "Could not set cursor hit test for window {}: {}", - window.title, err - ); - } - } - - if window.decorations != cache.window.decorations + if window.decorations != cache.decorations && window.decorations != winit_window.is_decorated() { winit_window.set_decorations(window.decorations); } - if window.resizable != cache.window.resizable + if window.resizable != cache.resizable && window.resizable != winit_window.is_resizable() { winit_window.set_resizable(window.resizable); } - if window.enabled_buttons != cache.window.enabled_buttons { + if window.enabled_buttons != cache.enabled_buttons { winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons)); } - if window.resize_constraints != cache.window.resize_constraints { + if window.resize_constraints != cache.resize_constraints { let constraints = window.resize_constraints.check_constraints(); let min_inner_size = LogicalSize { width: constraints.min_width, @@ -461,7 +444,7 @@ pub(crate) fn changed_windows( } } - if window.position != cache.window.position { + if window.position != cache.position { if let Some(position) = crate::winit_window_position( &window.position, &window.resolution, @@ -502,62 +485,62 @@ pub(crate) fn changed_windows( } } - if window.focused != cache.window.focused && window.focused { + if window.focused != cache.focused && window.focused { winit_window.focus_window(); } - if window.window_level != cache.window.window_level { + if window.window_level != cache.window_level { winit_window.set_window_level(convert_window_level(window.window_level)); } // Currently unsupported changes - if window.transparent != cache.window.transparent { - window.transparent = cache.window.transparent; + if window.transparent != cache.transparent { + window.transparent = cache.transparent; warn!("Winit does not currently support updating transparency after window creation."); } #[cfg(target_arch = "wasm32")] - if window.canvas != cache.window.canvas { - window.canvas.clone_from(&cache.window.canvas); + if window.canvas != cache.canvas { + window.canvas.clone_from(&cache.canvas); warn!( "Bevy currently doesn't support modifying the window canvas after initialization." ); } - if window.ime_enabled != cache.window.ime_enabled { + if window.ime_enabled != cache.ime_enabled { winit_window.set_ime_allowed(window.ime_enabled); } - if window.ime_position != cache.window.ime_position { + if window.ime_position != cache.ime_position { winit_window.set_ime_cursor_area( LogicalPosition::new(window.ime_position.x, window.ime_position.y), PhysicalSize::new(10, 10), ); } - if window.window_theme != cache.window.window_theme { + if window.window_theme != cache.window_theme { winit_window.set_theme(window.window_theme.map(convert_window_theme)); } - if window.visible != cache.window.visible { + if window.visible != cache.visible { winit_window.set_visible(window.visible); } #[cfg(target_os = "ios")] { - if window.recognize_pinch_gesture != cache.window.recognize_pinch_gesture { + if window.recognize_pinch_gesture != cache.recognize_pinch_gesture { winit_window.recognize_pinch_gesture(window.recognize_pinch_gesture); } - if window.recognize_rotation_gesture != cache.window.recognize_rotation_gesture { + if window.recognize_rotation_gesture != cache.recognize_rotation_gesture { winit_window.recognize_rotation_gesture(window.recognize_rotation_gesture); } - if window.recognize_doubletap_gesture != cache.window.recognize_doubletap_gesture { + if window.recognize_doubletap_gesture != cache.recognize_doubletap_gesture { winit_window.recognize_doubletap_gesture(window.recognize_doubletap_gesture); } - if window.recognize_pan_gesture != cache.window.recognize_pan_gesture { + if window.recognize_pan_gesture != cache.recognize_pan_gesture { match ( window.recognize_pan_gesture, - cache.window.recognize_pan_gesture, + cache.recognize_pan_gesture, ) { (Some(_), Some(_)) => { warn!("Bevy currently doesn't support modifying PanGesture number of fingers recognition. Please disable it before re-enabling it with the new number of fingers"); @@ -567,16 +550,15 @@ pub(crate) fn changed_windows( } } - if window.prefers_home_indicator_hidden != cache.window.prefers_home_indicator_hidden { + if window.prefers_home_indicator_hidden != cache.prefers_home_indicator_hidden { winit_window .set_prefers_home_indicator_hidden(window.prefers_home_indicator_hidden); } - if window.prefers_status_bar_hidden != cache.window.prefers_status_bar_hidden { + if window.prefers_status_bar_hidden != cache.prefers_status_bar_hidden { winit_window.set_prefers_status_bar_hidden(window.prefers_status_bar_hidden); } if window.preferred_screen_edges_deferring_system_gestures != cache - .window .preferred_screen_edges_deferring_system_gestures { use crate::converters::convert_screen_edge; @@ -585,7 +567,59 @@ pub(crate) fn changed_windows( winit_window.set_preferred_screen_edges_deferring_system_gestures(preferred_edge); } } - cache.window = window.clone(); + **cache = window.clone(); + } + }); +} + +pub(crate) fn changed_cursor_options( + mut changed_windows: Query< + ( + Entity, + &Window, + &mut CursorOptions, + &mut CachedCursorOptions, + ), + Changed, + >, + _non_send_marker: NonSendMarker, +) { + WINIT_WINDOWS.with_borrow(|winit_windows| { + for (entity, window, mut cursor_options, mut cache) in &mut changed_windows { + // This system already only runs when the cursor options change, so we need to bypass change detection or the next frame will also run this system + let cursor_options = cursor_options.bypass_change_detection(); + let Some(winit_window) = winit_windows.get_window(entity) else { + continue; + }; + // Don't check the cache for the grab mode. It can change through external means, leaving the cache outdated. + if let Err(err) = + crate::winit_windows::attempt_grab(winit_window, cursor_options.grab_mode) + { + warn!( + "Could not set cursor grab mode for window {}: {}", + window.title, err + ); + cursor_options.grab_mode = cache.grab_mode; + } else { + cache.grab_mode = cursor_options.grab_mode; + } + + if cursor_options.visible != cache.visible { + winit_window.set_cursor_visible(cursor_options.visible); + cache.visible = cursor_options.visible; + } + + if cursor_options.hit_test != cache.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(cursor_options.hit_test) { + warn!( + "Could not set cursor hit test for window {}: {}", + window.title, err + ); + cursor_options.hit_test = cache.hit_test; + } else { + cache.hit_test = cursor_options.hit_test; + } + } } }); } diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 8bf326f453..5110e670c2 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -4,8 +4,8 @@ use bevy_ecs::entity::Entity; use bevy_ecs::entity::EntityHashMap; use bevy_platform::collections::HashMap; use bevy_window::{ - CursorGrabMode, MonitorSelection, VideoModeSelection, Window, WindowMode, WindowPosition, - WindowResolution, WindowWrapper, + CursorGrabMode, CursorOptions, MonitorSelection, VideoModeSelection, Window, WindowMode, + WindowPosition, WindowResolution, WindowWrapper, }; use tracing::warn; @@ -58,6 +58,7 @@ impl WinitWindows { event_loop: &ActiveEventLoop, entity: Entity, window: &Window, + cursor_options: &CursorOptions, adapters: &mut AccessKitAdapters, handlers: &mut WinitActionRequestHandlers, accessibility_requested: &AccessibilityRequested, @@ -310,16 +311,16 @@ impl WinitWindows { winit_window.set_visible(window.visible); // Do not set the grab mode on window creation if it's none. It can fail on mobile. - if window.cursor_options.grab_mode != CursorGrabMode::None { - let _ = attempt_grab(&winit_window, window.cursor_options.grab_mode); + if cursor_options.grab_mode != CursorGrabMode::None { + let _ = attempt_grab(&winit_window, cursor_options.grab_mode); } - winit_window.set_cursor_visible(window.cursor_options.visible); + winit_window.set_cursor_visible(cursor_options.visible); // Do not set the cursor hittest on window creation if it's false, as it will always fail on // some platforms and log an unfixable warning. - if !window.cursor_options.hit_test { - if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) { + if !cursor_options.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(cursor_options.hit_test) { warn!( "Could not set cursor hit test for window {}: {}", window.title, err diff --git a/examples/games/desk_toy.rs b/examples/games/desk_toy.rs index c25286dd9c..b5c638348e 100644 --- a/examples/games/desk_toy.rs +++ b/examples/games/desk_toy.rs @@ -10,7 +10,7 @@ use bevy::{ app::AppExit, input::common_conditions::{input_just_pressed, input_just_released}, prelude::*, - window::{PrimaryWindow, WindowLevel}, + window::{CursorOptions, PrimaryWindow, WindowLevel}, }; #[cfg(target_os = "macos")] @@ -219,12 +219,13 @@ fn get_cursor_world_pos( /// Update whether the window is clickable or not fn update_cursor_hit_test( cursor_world_pos: Res, - mut primary_window: Single<&mut Window, With>, + primary_window: Single<(&Window, &mut CursorOptions), With>, bevy_logo_transform: Single<&Transform, With>, ) { + let (window, mut cursor_options) = primary_window.into_inner(); // If the window has decorations (e.g. a border) then it should be clickable - if primary_window.decorations { - primary_window.cursor_options.hit_test = true; + if window.decorations { + cursor_options.hit_test = true; return; } @@ -234,7 +235,7 @@ fn update_cursor_hit_test( }; // If the cursor is within the radius of the Bevy logo make the window clickable otherwise the window is not clickable - primary_window.cursor_options.hit_test = bevy_logo_transform + cursor_options.hit_test = bevy_logo_transform .translation .truncate() .distance(cursor_world_pos) diff --git a/examples/helpers/camera_controller.rs b/examples/helpers/camera_controller.rs index a60aa69a5e..3f6e4ed477 100644 --- a/examples/helpers/camera_controller.rs +++ b/examples/helpers/camera_controller.rs @@ -8,7 +8,7 @@ use bevy::{ input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseScrollUnit}, prelude::*, - window::CursorGrabMode, + window::{CursorGrabMode, CursorOptions}, }; use std::{f32::consts::*, fmt}; @@ -126,7 +126,7 @@ Freecam Controls: fn run_camera_controller( time: Res { - type Fetch<'w, 's> = AssetChangedFetch<'w, A>; + type Fetch<'w> = AssetChangedFetch<'w, A>; type State = AssetChangedState; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -165,7 +163,7 @@ unsafe impl WorldQuery for AssetChanged { state: &'s Self::State, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { // SAFETY: // - `AssetChanges` is private and only accessed mutably in the `AssetEventSystems` system set. // - `resource_id` was obtained from the type ID of `AssetChanges`. @@ -204,7 +202,7 @@ unsafe impl WorldQuery for AssetChanged { const IS_DENSE: bool = <&A>::IS_DENSE; unsafe fn set_archetype<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, state: &'s Self::State, archetype: &'w Archetype, table: &'w Table, @@ -218,7 +216,7 @@ unsafe impl WorldQuery for AssetChanged { } unsafe fn set_table<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table, ) { @@ -271,14 +269,15 @@ unsafe impl QueryFilter for AssetChanged { #[inline] unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_, '_>, + state: &Self::State, + fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, ) -> bool { 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_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index 44021f27a7..910d9ce3b6 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -74,13 +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.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(0, parse_quote!('__s)); 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_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 { @@ -165,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, @@ -200,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, @@ -257,7 +267,7 @@ 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, '__s> = #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, '__s>( item: Self::Item<'__wlong, '__s> @@ -280,12 +290,13 @@ 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, '__s>( - _fetch: &mut ::Fetch<'__w, '__s>, + _state: &'__s Self::State, + _fetch: &mut ::Fetch<'__w>, _entity: #path::entity::Entity, _table_row: #path::storage::TableRow, ) -> 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),)* } } } @@ -314,7 +325,7 @@ 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, '__s> = #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, '__s>( item: Self::Item<'__wlong, '__s> @@ -337,12 +348,13 @@ 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, '__s>( - _fetch: &mut ::Fetch<'__w, '__s>, + _state: &'__s Self::State, + _fetch: &mut ::Fetch<'__w>, _entity: #path::entity::Entity, _table_row: #path::storage::TableRow, ) -> 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),)* } } } diff --git a/crates/bevy_ecs/macros/src/query_filter.rs b/crates/bevy_ecs/macros/src/query_filter.rs index acc5c3d41b..5ae2d2325f 100644 --- a/crates/bevy_ecs/macros/src/query_filter.rs +++ b/crates/bevy_ecs/macros/src/query_filter.rs @@ -23,7 +23,6 @@ pub fn derive_query_filter_impl(input: TokenStream) -> TokenStream { let user_generics_with_world = { let mut generics = ast.generics; generics.params.insert(0, parse_quote!('__w)); - generics.params.insert(0, parse_quote!('__s)); generics }; let (user_impl_generics_with_world, user_ty_generics_with_world, user_where_clauses_with_world) = @@ -102,12 +101,13 @@ pub fn derive_query_filter_impl(input: TokenStream) -> TokenStream { #[allow(unused_variables)] #[inline(always)] - unsafe fn filter_fetch<'__w, '__s>( - _fetch: &mut ::Fetch<'__w, '__s>, + 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 c064b05b97..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 { + #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( + #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; }, } } @@ -78,8 +78,8 @@ 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, '__s>,)* - #marker_name: (&'__w(), &'__s()), + #(#named_field_idents: <#field_types as #path::query::WorldQuery>::Fetch<'__w>,)* + #marker_name: &'__w(), } impl #user_impl_generics_with_world Clone for #fetch_struct_name #user_ty_generics_with_world @@ -87,7 +87,7 @@ pub(crate) fn world_query_impl( fn clone(&self) -> Self { Self { #(#named_field_idents: self.#named_field_idents.clone(),)* - #marker_name: (&(), &()), + #marker_name: &(), } } } @@ -96,17 +96,17 @@ pub(crate) fn world_query_impl( unsafe impl #user_impl_generics #path::query::WorldQuery for #struct_name #user_ty_generics #user_where_clauses { - type Fetch<'__w, '__s> = #fetch_struct_name #user_ty_generics_with_world; + type Fetch<'__w> = #fetch_struct_name #user_ty_generics_with_world; type State = #state_struct_name #user_ty_generics; - fn shrink_fetch<'__wlong: '__wshort, '__wshort, '__s>( - fetch: <#struct_name #user_ty_generics as #path::query::WorldQuery>::Fetch<'__wlong, '__s> - ) -> <#struct_name #user_ty_generics as #path::query::WorldQuery>::Fetch<'__wshort, '__s> { + fn shrink_fetch<'__wlong: '__wshort, '__wshort>( + fetch: <#struct_name #user_ty_generics as #path::query::WorldQuery>::Fetch<'__wlong> + ) -> <#struct_name #user_ty_generics as #path::query::WorldQuery>::Fetch<'__wshort> { #fetch_struct_name { #( #named_field_idents: <#field_types>::shrink_fetch(fetch.#named_field_idents), )* - #marker_name: (&(), &()), + #marker_name: &(), } } @@ -115,7 +115,7 @@ pub(crate) fn world_query_impl( state: &'__s Self::State, _last_run: #path::component::Tick, _this_run: #path::component::Tick, - ) -> ::Fetch<'__w, '__s> { + ) -> ::Fetch<'__w> { #fetch_struct_name { #(#named_field_idents: <#field_types>::init_fetch( @@ -125,7 +125,7 @@ pub(crate) fn world_query_impl( _this_run, ), )* - #marker_name: (&(), &()), + #marker_name: &(), } } @@ -134,7 +134,7 @@ pub(crate) fn world_query_impl( /// SAFETY: we call `set_archetype` for each member that implements `Fetch` #[inline] unsafe fn set_archetype<'__w, '__s>( - _fetch: &mut ::Fetch<'__w, '__s>, + _fetch: &mut ::Fetch<'__w>, _state: &'__s Self::State, _archetype: &'__w #path::archetype::Archetype, _table: &'__w #path::storage::Table @@ -145,7 +145,7 @@ pub(crate) fn world_query_impl( /// SAFETY: we call `set_table` for each member that implements `Fetch` #[inline] unsafe fn set_table<'__w, '__s>( - _fetch: &mut ::Fetch<'__w, '__s>, + _fetch: &mut ::Fetch<'__w>, _state: &'__s Self::State, _table: &'__w #path::storage::Table ) { diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 4abdbbe530..0a13b61819 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -326,7 +326,8 @@ pub unsafe trait QueryData: WorldQuery { /// `table_row` must be in the range of the current table and archetype. /// - There must not be simultaneous conflicting component access registered in `update_component_access`. unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, ) -> Self::Item<'w, 's>; @@ -359,27 +360,24 @@ pub trait ReleaseStateQueryData: QueryData { /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for Entity { - type Fetch<'w, 's> = (); + type Fetch<'w> = (); type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - _: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { - } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, _state: &'s Self::State, _last_run: Tick, _this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { } const IS_DENSE: bool = true; #[inline] unsafe fn set_archetype<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, @@ -388,7 +386,7 @@ unsafe impl WorldQuery for Entity { #[inline] unsafe fn set_table<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _table: &'w Table, ) { @@ -425,7 +423,8 @@ unsafe impl QueryData for Entity { #[inline(always)] unsafe fn fetch<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + _fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -446,12 +445,10 @@ impl ReleaseStateQueryData for Entity { /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for EntityLocation { - type Fetch<'w, 's> = &'w Entities; + type Fetch<'w> = &'w Entities; type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -460,7 +457,7 @@ unsafe impl WorldQuery for EntityLocation { _state: &'s Self::State, _last_run: Tick, _this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { world.entities() } @@ -470,7 +467,7 @@ unsafe impl WorldQuery for EntityLocation { #[inline] unsafe fn set_archetype<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, @@ -479,7 +476,7 @@ unsafe impl WorldQuery for EntityLocation { #[inline] unsafe fn set_table<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _table: &'w Table, ) { @@ -515,7 +512,8 @@ unsafe impl QueryData for EntityLocation { #[inline(always)] unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -606,12 +604,10 @@ pub struct SpawnDetailsFetch<'w> { // SAFETY: // No components are accessed. unsafe impl WorldQuery for SpawnDetails { - type Fetch<'w, 's> = SpawnDetailsFetch<'w>; + type Fetch<'w> = SpawnDetailsFetch<'w>; type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -620,7 +616,7 @@ unsafe impl WorldQuery for SpawnDetails { _state: &'s Self::State, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { SpawnDetailsFetch { entities: world.entities(), last_run, @@ -632,7 +628,7 @@ unsafe impl WorldQuery for SpawnDetails { #[inline] unsafe fn set_archetype<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table, @@ -641,7 +637,7 @@ unsafe impl WorldQuery for SpawnDetails { #[inline] unsafe fn set_table<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _table: &'w Table, ) { @@ -679,7 +675,8 @@ unsafe impl QueryData for SpawnDetails { #[inline(always)] unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -722,12 +719,10 @@ pub struct EntityFetch<'w> { /// This is sound because `update_component_access` sets read access for all components and panic when appropriate. /// Filters are unchanged. unsafe impl<'a> WorldQuery for EntityRef<'a> { - type Fetch<'w, 's> = EntityFetch<'w>; + type Fetch<'w> = EntityFetch<'w>; type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -736,7 +731,7 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { _state: &'s Self::State, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { EntityFetch { world, last_run, @@ -748,7 +743,7 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { #[inline] unsafe fn set_archetype<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, @@ -757,7 +752,7 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { #[inline] unsafe fn set_table<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _table: &'w Table, ) { @@ -799,7 +794,8 @@ unsafe impl<'a> QueryData for EntityRef<'a> { #[inline(always)] unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -826,12 +822,10 @@ impl ReleaseStateQueryData for EntityRef<'_> { /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for EntityMut<'a> { - type Fetch<'w, 's> = EntityFetch<'w>; + type Fetch<'w> = EntityFetch<'w>; type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -840,7 +834,7 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { _state: &'s Self::State, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { EntityFetch { world, last_run, @@ -852,7 +846,7 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { #[inline] unsafe fn set_archetype<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, @@ -861,7 +855,7 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { #[inline] unsafe fn set_table<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _table: &'w Table, ) { @@ -903,7 +897,8 @@ unsafe impl<'a> QueryData for EntityMut<'a> { #[inline(always)] unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -927,12 +922,10 @@ impl ReleaseStateQueryData for EntityMut<'_> { /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { - type Fetch<'w, 's> = (EntityFetch<'w>, Access); + type Fetch<'w> = (EntityFetch<'w>, Access); type State = Access; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -943,7 +936,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { _state: &'s Self::State, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { let mut access = Access::default(); access.read_all_components(); ( @@ -958,7 +951,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { #[inline] unsafe fn set_archetype<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, state: &'s Self::State, _: &'w Archetype, _table: &Table, @@ -967,11 +960,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { } #[inline] - unsafe fn set_table<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, - state: &'s Self::State, - _: &'w Table, - ) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, _: &'w Table) { fetch.1.clone_from(state); } @@ -1035,7 +1024,8 @@ unsafe impl<'a> QueryData for FilteredEntityRef<'a> { #[inline(always)] unsafe fn fetch<'w, 's>( - (fetch, access): &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + (fetch, access): &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -1056,12 +1046,10 @@ unsafe impl ReadOnlyQueryData for FilteredEntityRef<'_> {} /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { - type Fetch<'w, 's> = (EntityFetch<'w>, Access); + type Fetch<'w> = (EntityFetch<'w>, Access); type State = Access; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -1072,7 +1060,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { _state: &'s Self::State, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { let mut access = Access::default(); access.write_all_components(); ( @@ -1087,7 +1075,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { #[inline] unsafe fn set_archetype<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, state: &'s Self::State, _: &'w Archetype, _table: &Table, @@ -1096,11 +1084,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { } #[inline] - unsafe fn set_table<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, - state: &'s Self::State, - _: &'w Table, - ) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, _: &'w Table) { fetch.1.clone_from(state); } @@ -1162,7 +1146,8 @@ unsafe impl<'a> QueryData for FilteredEntityMut<'a> { #[inline(always)] unsafe fn fetch<'w, 's>( - (fetch, access): &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + (fetch, access): &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -1185,12 +1170,10 @@ unsafe impl<'a, B> WorldQuery for EntityRefExcept<'a, B> where B: Bundle, { - type Fetch<'w, 's> = EntityFetch<'w>; + type Fetch<'w> = EntityFetch<'w>; type State = SmallVec<[ComponentId; 4]>; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -1199,7 +1182,7 @@ where _: &'s Self::State, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { EntityFetch { world, last_run, @@ -1210,14 +1193,14 @@ where const IS_DENSE: bool = true; unsafe fn set_archetype<'w, 's>( - _: &mut Self::Fetch<'w, 's>, + _: &mut Self::Fetch<'w>, _: &'s Self::State, _: &'w Archetype, _: &'w Table, ) { } - unsafe fn set_table<'w, 's>(_: &mut Self::Fetch<'w, 's>, _: &'s Self::State, _: &'w Table) {} + unsafe fn set_table<'w, 's>(_: &mut Self::Fetch<'w>, _: &'s Self::State, _: &'w Table) {} fn update_component_access( state: &Self::State, @@ -1273,7 +1256,8 @@ where } unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, _: TableRow, ) -> Self::Item<'w, 's> { @@ -1296,12 +1280,10 @@ unsafe impl<'a, B> WorldQuery for EntityMutExcept<'a, B> where B: Bundle, { - type Fetch<'w, 's> = EntityFetch<'w>; + type Fetch<'w> = EntityFetch<'w>; type State = SmallVec<[ComponentId; 4]>; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -1310,7 +1292,7 @@ where _: &'s Self::State, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { EntityFetch { world, last_run, @@ -1321,14 +1303,14 @@ where const IS_DENSE: bool = true; unsafe fn set_archetype<'w, 's>( - _: &mut Self::Fetch<'w, 's>, + _: &mut Self::Fetch<'w>, _: &'s Self::State, _: &'w Archetype, _: &'w Table, ) { } - unsafe fn set_table<'w, 's>(_: &mut Self::Fetch<'w, 's>, _: &'s Self::State, _: &'w Table) {} + unsafe fn set_table<'w, 's>(_: &mut Self::Fetch<'w>, _: &'s Self::State, _: &'w Table) {} fn update_component_access( state: &Self::State, @@ -1385,7 +1367,8 @@ where } unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, _: TableRow, ) -> Self::Item<'w, 's> { @@ -1401,12 +1384,10 @@ where /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for &Archetype { - type Fetch<'w, 's> = (&'w Entities, &'w Archetypes); + type Fetch<'w> = (&'w Entities, &'w Archetypes); type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -1415,7 +1396,7 @@ unsafe impl WorldQuery for &Archetype { _state: &'s Self::State, _last_run: Tick, _this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { (world.entities(), world.archetypes()) } @@ -1425,7 +1406,7 @@ unsafe impl WorldQuery for &Archetype { #[inline] unsafe fn set_archetype<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, @@ -1434,7 +1415,7 @@ unsafe impl WorldQuery for &Archetype { #[inline] unsafe fn set_table<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _table: &'w Table, ) { @@ -1470,7 +1451,8 @@ unsafe impl QueryData for &Archetype { #[inline(always)] unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -1515,12 +1497,10 @@ impl Copy for ReadFetch<'_, T> {} /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl WorldQuery for &T { - type Fetch<'w, 's> = ReadFetch<'w, T>; + type Fetch<'w> = ReadFetch<'w, T>; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -1625,7 +1605,8 @@ unsafe impl QueryData for &T { #[inline(always)] unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -1692,12 +1673,10 @@ impl Copy for RefFetch<'_, T> {} /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { - type Fetch<'w, 's> = RefFetch<'w, T>; + type Fetch<'w> = RefFetch<'w, T>; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -1811,7 +1790,8 @@ unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> { #[inline(always)] unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -1901,12 +1881,10 @@ impl Copy for WriteFetch<'_, T> {} /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { - type Fetch<'w, 's> = WriteFetch<'w, T>; + type Fetch<'w> = WriteFetch<'w, T>; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -2020,7 +1998,8 @@ unsafe impl<'__w, T: Component> QueryData for &'__w mut T #[inline(always)] unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -2085,12 +2064,10 @@ impl> ReleaseStateQueryData for &mut T { /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { - type Fetch<'w, 's> = WriteFetch<'w, T>; + type Fetch<'w> = WriteFetch<'w, T>; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -2175,13 +2152,14 @@ unsafe impl<'__w, T: Component> QueryData for Mut<'__w, T> #[inline(always)] // Forwarded to `&mut T` unsafe fn fetch<'w, 's>( + state: &'s Self::State, // Rust complains about lifetime bounds not matching the trait if I directly use `WriteFetch<'w, T>` right here. // But it complains nowhere else in the entire trait implementation. - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, ) -> Self::Item<'w, 's> { - <&mut T as QueryData>::fetch(fetch, entity, table_row) + <&mut T as QueryData>::fetch(state, fetch, entity, table_row) } } @@ -2192,12 +2170,12 @@ impl> ReleaseStateQueryData for Mut<'_, T> { } #[doc(hidden)] -pub struct OptionFetch<'w, 's, T: WorldQuery> { - fetch: T::Fetch<'w, 's>, +pub struct OptionFetch<'w, T: WorldQuery> { + fetch: T::Fetch<'w>, matches: bool, } -impl Clone for OptionFetch<'_, '_, T> { +impl Clone for OptionFetch<'_, T> { fn clone(&self) -> Self { Self { fetch: self.fetch.clone(), @@ -2211,12 +2189,10 @@ impl Clone for OptionFetch<'_, '_, T> { /// This is sound because `update_component_access` adds the same accesses as `T`. /// Filters are unchanged. unsafe impl WorldQuery for Option { - type Fetch<'w, 's> = OptionFetch<'w, 's, T>; + type Fetch<'w> = OptionFetch<'w, T>; type State = T::State; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { OptionFetch { fetch: T::shrink_fetch(fetch.fetch), matches: fetch.matches, @@ -2229,7 +2205,7 @@ unsafe impl WorldQuery for Option { state: &'s T::State, last_run: Tick, this_run: Tick, - ) -> OptionFetch<'w, 's, T> { + ) -> OptionFetch<'w, T> { OptionFetch { // SAFETY: The invariants are upheld by the caller. fetch: unsafe { T::init_fetch(world, state, last_run, this_run) }, @@ -2241,7 +2217,7 @@ unsafe impl WorldQuery for Option { #[inline] unsafe fn set_archetype<'w, 's>( - fetch: &mut OptionFetch<'w, 's, T>, + fetch: &mut OptionFetch<'w, T>, state: &'s T::State, archetype: &'w Archetype, table: &'w Table, @@ -2257,7 +2233,7 @@ unsafe impl WorldQuery for Option { #[inline] unsafe fn set_table<'w, 's>( - fetch: &mut OptionFetch<'w, 's, T>, + fetch: &mut OptionFetch<'w, T>, state: &'s T::State, table: &'w Table, ) { @@ -2315,14 +2291,15 @@ unsafe impl QueryData for Option { #[inline(always)] unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, ) -> Self::Item<'w, 's> { fetch .matches // SAFETY: The invariants are upheld by the caller. - .then(|| unsafe { T::fetch(&mut fetch.fetch, entity, table_row) }) + .then(|| unsafe { T::fetch(state, &mut fetch.fetch, entity, table_row) }) } } @@ -2410,12 +2387,10 @@ impl core::fmt::Debug for Has { /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for Has { - type Fetch<'w, 's> = bool; + type Fetch<'w> = bool; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -2425,7 +2400,7 @@ unsafe impl WorldQuery for Has { _state: &'s Self::State, _last_run: Tick, _this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { false } @@ -2438,7 +2413,7 @@ unsafe impl WorldQuery for Has { #[inline] unsafe fn set_archetype<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, state: &'s Self::State, archetype: &'w Archetype, _table: &Table, @@ -2448,7 +2423,7 @@ unsafe impl WorldQuery for Has { #[inline] unsafe fn set_table<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, state: &'s Self::State, table: &'w Table, ) { @@ -2493,7 +2468,8 @@ unsafe impl QueryData for Has { #[inline(always)] unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -2518,7 +2494,7 @@ impl ReleaseStateQueryData for Has { pub struct AnyOf(PhantomData); macro_rules! impl_tuple_query_data { - ($(#[$meta:meta])* $(($name: ident, $item: ident)),*) => { + ($(#[$meta:meta])* $(($name: ident, $item: ident, $state: ident)),*) => { #[expect( clippy::allow_attributes, reason = "This is a tuple-related macro; as such the lints below may not always apply." @@ -2561,13 +2537,15 @@ macro_rules! impl_tuple_query_data { #[inline(always)] unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow ) -> Self::Item<'w, 's> { + let ($($state,)*) = state; let ($($name,)*) = fetch; // SAFETY: The invariants are upheld by the caller. - ($(unsafe { $name::fetch($name, entity, table_row) },)*) + ($(unsafe { $name::fetch($state, $name, entity, table_row) },)*) } } @@ -2615,10 +2593,10 @@ macro_rules! impl_anytuple_fetch { /// `update_component_access` replaces the filters with a disjunction where every element is a conjunction of the previous filters and the filters of one of the subqueries. /// This is sound because `matches_component_set` returns a disjunction of the results of the subqueries' implementations. unsafe impl<$($name: WorldQuery),*> WorldQuery for AnyOf<($($name,)*)> { - type Fetch<'w, 's> = ($(($name::Fetch<'w, 's>, bool),)*); + type Fetch<'w> = ($(($name::Fetch<'w>, bool),)*); type State = ($($name::State,)*); - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>(fetch: Self::Fetch<'wlong, 's>) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { let ($($name,)*) = fetch; ($( ($name::shrink_fetch($name.0), $name.1), @@ -2626,7 +2604,7 @@ macro_rules! impl_anytuple_fetch { } #[inline] - unsafe fn init_fetch<'w, 's>(_world: UnsafeWorldCell<'w>, state: &'s Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w, 's> { + unsafe fn init_fetch<'w, 's>(_world: UnsafeWorldCell<'w>, state: &'s Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w> { let ($($name,)*) = state; // SAFETY: The invariants are upheld by the caller. ($(( unsafe { $name::init_fetch(_world, $name, _last_run, _this_run) }, false),)*) @@ -2636,7 +2614,7 @@ macro_rules! impl_anytuple_fetch { #[inline] unsafe fn set_archetype<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table @@ -2653,7 +2631,7 @@ macro_rules! impl_anytuple_fetch { } #[inline] - unsafe fn set_table<'w, 's>(_fetch: &mut Self::Fetch<'w, 's>, _state: &'s Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>(_fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _table: &'w Table) { let ($($name,)*) = _fetch; let ($($state,)*) = _state; $( @@ -2736,14 +2714,16 @@ macro_rules! impl_anytuple_fetch { #[inline(always)] unsafe fn fetch<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow ) -> Self::Item<'w, 's> { let ($($name,)*) = _fetch; + let ($($state,)*) = _state; ($( // SAFETY: The invariants are required to be upheld by the caller. - $name.1.then(|| unsafe { $name::fetch(&mut $name.0, _entity, _table_row) }), + $name.1.then(|| unsafe { $name::fetch($state, &mut $name.0, _entity, _table_row) }), )*) } } @@ -2774,7 +2754,8 @@ all_tuples!( 0, 15, F, - i + i, + s ); all_tuples!( #[doc(fake_variadic)] @@ -2795,12 +2776,10 @@ pub(crate) struct NopWorldQuery(PhantomData); /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for NopWorldQuery { - type Fetch<'w, 's> = (); + type Fetch<'w> = (); type State = D::State; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - _fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { } #[inline(always)] @@ -2857,7 +2836,8 @@ unsafe impl QueryData for NopWorldQuery { #[inline(always)] unsafe fn fetch<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -2875,13 +2855,11 @@ impl ReleaseStateQueryData for NopWorldQuery { /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for PhantomData { - type Fetch<'w, 's> = (); + type Fetch<'w> = (); type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - _fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { } unsafe fn init_fetch<'w, 's>( @@ -2889,7 +2867,7 @@ unsafe impl WorldQuery for PhantomData { _state: &'s Self::State, _last_run: Tick, _this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { } // `PhantomData` does not match any components, so all components it matches @@ -2897,7 +2875,7 @@ unsafe impl WorldQuery for PhantomData { const IS_DENSE: bool = true; unsafe fn set_archetype<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table, @@ -2905,7 +2883,7 @@ unsafe impl WorldQuery for PhantomData { } unsafe fn set_table<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _table: &'w Table, ) { @@ -2939,7 +2917,8 @@ unsafe impl QueryData for PhantomData { } unsafe fn fetch<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, ) -> Self::Item<'w, 's> { @@ -3077,12 +3056,12 @@ mod tests { /// `update_component_access` and `update_archetype_component_access` do nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for NonReleaseQueryData { - type Fetch<'w, 's> = (); + type Fetch<'w> = (); type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - _: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>( + _: Self::Fetch<'wlong>, + ) -> Self::Fetch<'wshort> { } unsafe fn init_fetch<'w, 's>( @@ -3090,14 +3069,14 @@ mod tests { _state: &'s Self::State, _last_run: Tick, _this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { } const IS_DENSE: bool = true; #[inline] unsafe fn set_archetype<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, @@ -3106,7 +3085,7 @@ mod tests { #[inline] unsafe fn set_table<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _table: &'w Table, ) { @@ -3146,7 +3125,8 @@ mod tests { #[inline(always)] unsafe fn fetch<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, ) -> Self::Item<'w, 's> { diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index f5700a33c6..312b330e04 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -104,7 +104,8 @@ pub unsafe trait QueryFilter: WorldQuery { /// Must always be called _after_ [`WorldQuery::set_table`] or [`WorldQuery::set_archetype`]. `entity` and /// `table_row` must be in the range of the current table and archetype. unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_, '_>, + state: &Self::State, + fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, ) -> bool; @@ -145,13 +146,10 @@ pub struct With(PhantomData); /// `update_component_access` adds a `With` filter for `T`. /// This is sound because `matches_component_set` returns whether the set contains the component. unsafe impl WorldQuery for With { - type Fetch<'w, 's> = (); + type Fetch<'w> = (); type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - _: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { - } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} #[inline] unsafe fn init_fetch( @@ -208,7 +206,8 @@ unsafe impl QueryFilter for With { #[inline(always)] unsafe fn filter_fetch( - _fetch: &mut Self::Fetch<'_, '_>, + _state: &Self::State, + _fetch: &mut Self::Fetch<'_>, _entity: Entity, _table_row: TableRow, ) -> bool { @@ -248,13 +247,10 @@ pub struct Without(PhantomData); /// `update_component_access` adds a `Without` filter for `T`. /// This is sound because `matches_component_set` returns whether the set does not contain the component. unsafe impl WorldQuery for Without { - type Fetch<'w, 's> = (); + type Fetch<'w> = (); type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - _: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { - } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} #[inline] unsafe fn init_fetch( @@ -311,7 +307,8 @@ unsafe impl QueryFilter for Without { #[inline(always)] unsafe fn filter_fetch( - _fetch: &mut Self::Fetch<'_, '_>, + _state: &Self::State, + _fetch: &mut Self::Fetch<'_>, _entity: Entity, _table_row: TableRow, ) -> bool { @@ -352,12 +349,12 @@ unsafe impl QueryFilter for Without { pub struct Or(PhantomData); #[doc(hidden)] -pub struct OrFetch<'w, 's, T: WorldQuery> { - fetch: T::Fetch<'w, 's>, +pub struct OrFetch<'w, T: WorldQuery> { + fetch: T::Fetch<'w>, matches: bool, } -impl Clone for OrFetch<'_, '_, T> { +impl Clone for OrFetch<'_, T> { fn clone(&self) -> Self { Self { fetch: self.fetch.clone(), @@ -391,10 +388,10 @@ macro_rules! impl_or_query_filter { /// `update_component_access` replace the filters with a disjunction where every element is a conjunction of the previous filters and the filters of one of the subqueries. /// This is sound because `matches_component_set` returns a disjunction of the results of the subqueries' implementations. unsafe impl<$($filter: QueryFilter),*> WorldQuery for Or<($($filter,)*)> { - type Fetch<'w, 's> = ($(OrFetch<'w, 's, $filter>,)*); + type Fetch<'w> = ($(OrFetch<'w, $filter>,)*); type State = ($($filter::State,)*); - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>(fetch: Self::Fetch<'wlong, 's>) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { let ($($filter,)*) = fetch; ($( OrFetch { @@ -407,7 +404,7 @@ macro_rules! impl_or_query_filter { const IS_DENSE: bool = true $(&& $filter::IS_DENSE)*; #[inline] - unsafe fn init_fetch<'w, 's>(world: UnsafeWorldCell<'w>, state: &'s Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w, 's> { + unsafe fn init_fetch<'w, 's>(world: UnsafeWorldCell<'w>, state: &'s Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { let ($($filter,)*) = state; ($(OrFetch { // SAFETY: The invariants are upheld by the caller. @@ -417,7 +414,7 @@ macro_rules! impl_or_query_filter { } #[inline] - unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w, 's>, state: &'s Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, table: &'w Table) { let ($($filter,)*) = fetch; let ($($state,)*) = state; $( @@ -431,7 +428,7 @@ macro_rules! impl_or_query_filter { #[inline] unsafe fn set_archetype<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, state: &'s Self::State, archetype: &'w Archetype, table: &'w Table @@ -502,20 +499,22 @@ macro_rules! impl_or_query_filter { #[inline(always)] unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_, '_>, + state: &Self::State, + fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow ) -> bool { + let ($($state,)*) = state; let ($($filter,)*) = fetch; // SAFETY: The invariants are upheld by the caller. - false $(|| ($filter.matches && unsafe { $filter::filter_fetch(&mut $filter.fetch, entity, table_row) }))* + false $(|| ($filter.matches && unsafe { $filter::filter_fetch($state, &mut $filter.fetch, entity, table_row) }))* } } }; } macro_rules! impl_tuple_query_filter { - ($(#[$meta:meta])* $($name: ident),*) => { + ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { #[expect( clippy::allow_attributes, reason = "This is a tuple-related macro; as such the lints below may not always apply." @@ -535,13 +534,15 @@ macro_rules! impl_tuple_query_filter { #[inline(always)] unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_, '_>, + state: &Self::State, + fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow ) -> bool { + let ($($state,)*) = state; let ($($name,)*) = fetch; // SAFETY: The invariants are upheld by the caller. - true $(&& unsafe { $name::filter_fetch($name, entity, table_row) })* + true $(&& unsafe { $name::filter_fetch($state, $name, entity, table_row) })* } } @@ -553,7 +554,8 @@ all_tuples!( impl_tuple_query_filter, 0, 15, - F + F, + S ); all_tuples!( #[doc(fake_variadic)] @@ -575,13 +577,10 @@ pub struct Allows(PhantomData); /// `update_component_access` adds an archetypal filter for `T`. /// This is sound because it doesn't affect the query unsafe impl WorldQuery for Allows { - type Fetch<'w, 's> = (); + type Fetch<'w> = (); type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - _: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { - } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} #[inline] unsafe fn init_fetch(_: UnsafeWorldCell, _: &ComponentId, _: Tick, _: Tick) {} @@ -619,7 +618,12 @@ unsafe impl QueryFilter for Allows { const IS_ARCHETYPAL: bool = true; #[inline(always)] - unsafe fn filter_fetch(_: &mut Self::Fetch<'_, '_>, _: Entity, _: TableRow) -> bool { + unsafe fn filter_fetch( + _: &Self::State, + _: &mut Self::Fetch<'_>, + _: Entity, + _: TableRow, + ) -> bool { true } } @@ -720,12 +724,10 @@ impl Clone for AddedFetch<'_, T> { /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl WorldQuery for Added { - type Fetch<'w, 's> = AddedFetch<'w, T>; + type Fetch<'w> = AddedFetch<'w, T>; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -735,8 +737,8 @@ unsafe impl WorldQuery for Added { &id: &'s ComponentId, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { - Self::Fetch::<'w, 's> { + ) -> Self::Fetch<'w> { + Self::Fetch::<'w> { ticks: StorageSwitch::new( || None, || { @@ -761,7 +763,7 @@ unsafe impl WorldQuery for Added { #[inline] unsafe fn set_archetype<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, component_id: &'s ComponentId, _archetype: &'w Archetype, table: &'w Table, @@ -776,7 +778,7 @@ unsafe impl WorldQuery for Added { #[inline] unsafe fn set_table<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, &component_id: &'s ComponentId, table: &'w Table, ) { @@ -819,7 +821,8 @@ unsafe impl QueryFilter for Added { const IS_ARCHETYPAL: bool = false; #[inline(always)] unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_, '_>, + _state: &Self::State, + fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, ) -> bool { @@ -948,12 +951,10 @@ impl Clone for ChangedFetch<'_, T> { /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl WorldQuery for Changed { - type Fetch<'w, 's> = ChangedFetch<'w, T>; + type Fetch<'w> = ChangedFetch<'w, T>; type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -963,8 +964,8 @@ unsafe impl WorldQuery for Changed { &id: &'s ComponentId, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { - Self::Fetch::<'w, 's> { + ) -> Self::Fetch<'w> { + Self::Fetch::<'w> { ticks: StorageSwitch::new( || None, || { @@ -989,7 +990,7 @@ unsafe impl WorldQuery for Changed { #[inline] unsafe fn set_archetype<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, component_id: &'s ComponentId, _archetype: &'w Archetype, table: &'w Table, @@ -1004,7 +1005,7 @@ unsafe impl WorldQuery for Changed { #[inline] unsafe fn set_table<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, &component_id: &'s ComponentId, table: &'w Table, ) { @@ -1048,7 +1049,8 @@ unsafe impl QueryFilter for Changed { #[inline(always)] unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_, '_>, + _state: &Self::State, + fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, ) -> bool { @@ -1147,12 +1149,10 @@ pub struct SpawnedFetch<'w> { // SAFETY: WorldQuery impl accesses no components or component ticks unsafe impl WorldQuery for Spawned { - type Fetch<'w, 's> = SpawnedFetch<'w>; + type Fetch<'w> = SpawnedFetch<'w>; type State = (); - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -1162,7 +1162,7 @@ unsafe impl WorldQuery for Spawned { _state: &'s (), last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { SpawnedFetch { entities: world.entities(), last_run, @@ -1174,7 +1174,7 @@ unsafe impl WorldQuery for Spawned { #[inline] unsafe fn set_archetype<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s (), _archetype: &'w Archetype, _table: &'w Table, @@ -1182,12 +1182,7 @@ unsafe impl WorldQuery for Spawned { } #[inline] - unsafe fn set_table<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, - _state: &'s (), - _table: &'w Table, - ) { - } + unsafe fn set_table<'w, 's>(_fetch: &mut Self::Fetch<'w>, _state: &'s (), _table: &'w Table) {} #[inline] fn update_component_access(_state: &(), _access: &mut FilteredAccess) {} @@ -1209,7 +1204,8 @@ unsafe impl QueryFilter for Spawned { #[inline(always)] unsafe fn filter_fetch( - fetch: &mut Self::Fetch<'_, '_>, + _state: &Self::State, + fetch: &mut Self::Fetch<'_>, entity: Entity, _table_row: TableRow, ) -> bool { diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index a3b3d02c24..eb49204434 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -225,14 +225,26 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let fetched = unsafe { !F::filter_fetch(&mut self.cursor.filter, *entity, row) }; + let fetched = unsafe { + !F::filter_fetch( + &self.query_state.filter_state, + &mut self.cursor.filter, + *entity, + row, + ) + }; if fetched { continue; } // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let item = D::fetch(&mut self.cursor.fetch, *entity, row); + let item = D::fetch( + &self.query_state.fetch_state, + &mut self.cursor.fetch, + *entity, + row, + ); accum = func(accum, item); } @@ -283,6 +295,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // Caller assures `index` in range of the current archetype. let fetched = unsafe { !F::filter_fetch( + &self.query_state.filter_state, &mut self.cursor.filter, archetype_entity.id(), archetype_entity.table_row(), @@ -296,6 +309,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // Caller assures `index` in range of the current archetype. let item = unsafe { D::fetch( + &self.query_state.fetch_state, &mut self.cursor.fetch, archetype_entity.id(), archetype_entity.table_row(), @@ -356,14 +370,26 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let filter_matched = unsafe { F::filter_fetch(&mut self.cursor.filter, entity, row) }; + let filter_matched = unsafe { + F::filter_fetch( + &self.query_state.filter_state, + &mut self.cursor.filter, + entity, + row, + ) + }; if !filter_matched { continue; } // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let item = D::fetch(&mut self.cursor.fetch, entity, row); + let item = D::fetch( + &self.query_state.fetch_state, + &mut self.cursor.fetch, + entity, + row, + ); accum = func(accum, item); } @@ -979,7 +1005,7 @@ where entities: &'w Entities, tables: &'w Tables, archetypes: &'w Archetypes, - fetch: D::Fetch<'w, 's>, + fetch: D::Fetch<'w>, query_state: &'s QueryState, } @@ -1043,7 +1069,14 @@ where // SAFETY: // - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype // - fetch is only called once for each entity. - unsafe { D::fetch(&mut self.fetch, entity, location.table_row) } + unsafe { + D::fetch( + &self.query_state.fetch_state, + &mut self.fetch, + entity, + location.table_row, + ) + } } } @@ -1123,8 +1156,8 @@ pub struct QueryManyIter<'w, 's, D: QueryData, F: QueryFilter, I: Iterator, - filter: F::Fetch<'w, 's>, + fetch: D::Fetch<'w>, + filter: F::Fetch<'w>, query_state: &'s QueryState, } @@ -1171,8 +1204,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> entities: &'w Entities, tables: &'w Tables, archetypes: &'w Archetypes, - fetch: &mut D::Fetch<'w, 's>, - filter: &mut F::Fetch<'w, 's>, + fetch: &mut D::Fetch<'w>, + filter: &mut F::Fetch<'w>, query_state: &'s QueryState, ) -> Option> { for entity_borrow in entity_iter { @@ -1204,11 +1237,20 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> // SAFETY: set_archetype was called prior. // `location.archetype_row` is an archetype index row in range of the current archetype, because if it was not, the match above would have `continue`d - if unsafe { F::filter_fetch(filter, entity, location.table_row) } { + if unsafe { + F::filter_fetch( + &query_state.filter_state, + filter, + entity, + location.table_row, + ) + } { // SAFETY: // - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype // - fetch is only called once for each entity. - return Some(unsafe { D::fetch(fetch, entity, location.table_row) }); + return Some(unsafe { + D::fetch(&query_state.fetch_state, fetch, entity, location.table_row) + }); } } None @@ -1919,7 +1961,7 @@ pub struct QuerySortedManyIter<'w, 's, D: QueryData, F: QueryFilter, I: Iterator entities: &'w Entities, tables: &'w Tables, archetypes: &'w Archetypes, - fetch: D::Fetch<'w, 's>, + fetch: D::Fetch<'w>, query_state: &'s QueryState, } @@ -1987,7 +2029,14 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> // SAFETY: // - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype // - fetch is only called once for each entity. - unsafe { D::fetch(&mut self.fetch, entity, location.table_row) } + unsafe { + D::fetch( + &self.query_state.fetch_state, + &mut self.fetch, + entity, + location.table_row, + ) + } } /// Get next result from the query @@ -2219,7 +2268,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< let ptr = values.as_mut_ptr().cast::>(); for (offset, cursor) in self.cursors.iter_mut().enumerate() { - ptr.add(offset).write(cursor.peek_last().unwrap()); + ptr.add(offset) + .write(cursor.peek_last(self.query_state).unwrap()); } Some(values.assume_init()) @@ -2313,8 +2363,8 @@ struct QueryIterationCursor<'w, 's, D: QueryData, F: QueryFilter> { storage_id_iter: core::slice::Iter<'s, StorageId>, table_entities: &'w [Entity], archetype_entities: &'w [ArchetypeEntity], - fetch: D::Fetch<'w, 's>, - filter: F::Fetch<'w, 's>, + fetch: D::Fetch<'w>, + filter: F::Fetch<'w>, // length of the table or length of the archetype, depending on whether both `D`'s and `F`'s fetches are dense current_len: u32, // either table row or archetype index, depending on whether both `D`'s and `F`'s fetches are dense @@ -2394,7 +2444,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { /// The result of `next` and any previous calls to `peek_last` with this row must have been /// dropped to prevent aliasing mutable references. #[inline] - unsafe fn peek_last(&mut self) -> Option> { + unsafe fn peek_last(&mut self, query_state: &'s QueryState) -> Option> { if self.current_row > 0 { let index = self.current_row - 1; if self.is_dense { @@ -2405,6 +2455,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - `*entity` and `index` are in the current table. unsafe { Some(D::fetch( + &query_state.fetch_state, &mut self.fetch, *entity, // SAFETY: This is from an exclusive range, so it can't be max. @@ -2420,6 +2471,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - `archetype_entity.id()` and `archetype_entity.table_row()` are in the current archetype. unsafe { Some(D::fetch( + &query_state.fetch_state, &mut self.fetch, archetype_entity.id(), archetype_entity.table_row(), @@ -2488,7 +2540,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { unsafe { self.table_entities.get_unchecked(self.current_row as usize) }; // SAFETY: The row is less than the u32 len, so it must not be max. let row = unsafe { TableRow::new(NonMaxU32::new_unchecked(self.current_row)) }; - if !F::filter_fetch(&mut self.filter, *entity, row) { + if !F::filter_fetch(&query_state.filter_state, &mut self.filter, *entity, row) { self.current_row += 1; continue; } @@ -2498,7 +2550,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - `current_row` must be a table row in range of the current table, // because if it was not, then the above would have been executed. // - fetch is only called once for each `entity`. - let item = unsafe { D::fetch(&mut self.fetch, *entity, row) }; + let item = + unsafe { D::fetch(&query_state.fetch_state, &mut self.fetch, *entity, row) }; self.current_row += 1; return Some(item); @@ -2540,6 +2593,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { .get_unchecked(self.current_row as usize) }; if !F::filter_fetch( + &query_state.filter_state, &mut self.filter, archetype_entity.id(), archetype_entity.table_row(), @@ -2555,6 +2609,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - fetch is only called once for each `archetype_entity`. let item = unsafe { D::fetch( + &query_state.fetch_state, &mut self.fetch, archetype_entity.id(), archetype_entity.table_row(), diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index db22c152f7..7c1487fde4 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -819,27 +819,24 @@ mod tests { /// SAFETY: /// `update_component_access` adds resource read access for `R`. unsafe impl WorldQuery for ReadsRData { - type Fetch<'w, 's> = (); + type Fetch<'w> = (); type State = ComponentId; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - _: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { - } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, _state: &'s Self::State, _last_run: Tick, _this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { } const IS_DENSE: bool = true; #[inline] unsafe fn set_archetype<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, @@ -848,7 +845,7 @@ mod tests { #[inline] unsafe fn set_table<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _table: &'w Table, ) { @@ -894,7 +891,8 @@ mod tests { #[inline(always)] unsafe fn fetch<'w, 's>( - _fetch: &mut Self::Fetch<'w, 's>, + _state: &'s Self::State, + _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, ) -> Self::Item<'w, 's> { diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index e856be7619..1c739927ac 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -42,7 +42,7 @@ use variadics_please::all_tuples; /// [`QueryFilter`]: crate::query::QueryFilter pub unsafe trait WorldQuery { /// Per archetype/table state retrieved by this [`WorldQuery`] to compute [`Self::Item`](crate::query::QueryData::Item) for each entity. - type Fetch<'w, 's>: Clone; + type Fetch<'w>: Clone; /// State used to construct a [`Self::Fetch`](WorldQuery::Fetch). This will be cached inside [`QueryState`](crate::query::QueryState), /// so it is best to move as much data / computation here as possible to reduce the cost of @@ -50,9 +50,7 @@ pub unsafe trait WorldQuery { type State: Send + Sync + Sized; /// This function manually implements subtyping for the query fetches. - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's>; + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort>; /// Creates a new instance of [`Self::Fetch`](WorldQuery::Fetch), /// by combining data from the [`World`] with the cached [`Self::State`](WorldQuery::State). @@ -69,7 +67,7 @@ pub unsafe trait WorldQuery { state: &'s Self::State, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's>; + ) -> Self::Fetch<'w>; /// Returns true if (and only if) every table of every archetype matched by this fetch contains /// all of the matched components. @@ -90,7 +88,7 @@ pub unsafe trait WorldQuery { /// - `table` must correspond to `archetype`. /// - `state` must be the [`State`](Self::State) that `fetch` was initialized with. unsafe fn set_archetype<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, state: &'s Self::State, archetype: &'w Archetype, table: &'w Table, @@ -104,7 +102,7 @@ pub unsafe trait WorldQuery { /// - `table` must be from the same [`World`] that [`WorldQuery::init_state`] was called on. /// - `state` must be the [`State`](Self::State) that `fetch` was initialized with. unsafe fn set_table<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, state: &'s Self::State, table: &'w Table, ); @@ -160,11 +158,11 @@ macro_rules! impl_tuple_world_query { /// `update_component_access` adds all `With` and `Without` filters from the subqueries. /// This is sound because `matches_component_set` always returns `false` if any the subqueries' implementations return `false`. unsafe impl<$($name: WorldQuery),*> WorldQuery for ($($name,)*) { - type Fetch<'w, 's> = ($($name::Fetch<'w, 's>,)*); + type Fetch<'w> = ($($name::Fetch<'w>,)*); type State = ($($name::State,)*); - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>(fetch: Self::Fetch<'wlong, 's>) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { let ($($name,)*) = fetch; ($( $name::shrink_fetch($name), @@ -172,7 +170,7 @@ macro_rules! impl_tuple_world_query { } #[inline] - unsafe fn init_fetch<'w, 's>(world: UnsafeWorldCell<'w>, state: &'s Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w, 's> { + unsafe fn init_fetch<'w, 's>(world: UnsafeWorldCell<'w>, state: &'s Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { let ($($name,)*) = state; // SAFETY: The invariants are upheld by the caller. ($(unsafe { $name::init_fetch(world, $name, last_run, this_run) },)*) @@ -182,7 +180,7 @@ macro_rules! impl_tuple_world_query { #[inline] unsafe fn set_archetype<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, state: &'s Self::State, archetype: &'w Archetype, table: &'w Table @@ -194,7 +192,7 @@ macro_rules! impl_tuple_world_query { } #[inline] - unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w, 's>, state: &'s Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, table: &'w Table) { let ($($name,)*) = fetch; let ($($state,)*) = state; // SAFETY: The invariants are upheld by the caller. diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index b29436debb..6e44301b18 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1582,8 +1582,18 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { D::set_archetype(&mut fetch, &self.state.fetch_state, archetype, table); F::set_archetype(&mut filter, &self.state.filter_state, archetype, table); - if F::filter_fetch(&mut filter, entity, location.table_row) { - Ok(D::fetch(&mut fetch, entity, location.table_row)) + if F::filter_fetch( + &self.state.filter_state, + &mut filter, + entity, + location.table_row, + ) { + Ok(D::fetch( + &self.state.fetch_state, + &mut fetch, + entity, + location.table_row, + )) } else { Err(QueryEntityError::QueryDoesNotMatch( entity, diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 6c618a287f..38d4333843 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -1030,7 +1030,7 @@ impl<'w> UnsafeEntityCell<'w> { // Table corresponds to archetype. State is the same state used to init fetch above. unsafe { Q::set_archetype(&mut fetch, &state, archetype, table) } // SAFETY: Called after set_archetype above. Entity and location are guaranteed to exist. - let item = unsafe { Q::fetch(&mut fetch, self.id(), location.table_row) }; + let item = unsafe { Q::fetch(&state, &mut fetch, self.id(), location.table_row) }; Some(Q::release_state(item)) } else { None diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index 35b500059e..b216b5fd32 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -289,12 +289,12 @@ mod render_entities_world_query_impls { /// SAFETY: defers completely to `&RenderEntity` implementation, /// and then only modifies the output safely. unsafe impl WorldQuery for RenderEntity { - type Fetch<'w, 's> = <&'static RenderEntity as WorldQuery>::Fetch<'w, 's>; + type Fetch<'w> = <&'static RenderEntity as WorldQuery>::Fetch<'w>; type State = <&'static RenderEntity as WorldQuery>::State; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>( + fetch: Self::Fetch<'wlong>, + ) -> Self::Fetch<'wshort> { fetch } @@ -304,7 +304,7 @@ mod render_entities_world_query_impls { component_id: &'s ComponentId, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. unsafe { <&RenderEntity as WorldQuery>::init_fetch(world, component_id, last_run, this_run) @@ -315,7 +315,7 @@ mod render_entities_world_query_impls { #[inline] unsafe fn set_archetype<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, component_id: &'s ComponentId, archetype: &'w Archetype, table: &'w Table, @@ -328,7 +328,7 @@ mod render_entities_world_query_impls { #[inline] unsafe fn set_table<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, &component_id: &'s ComponentId, table: &'w Table, ) { @@ -374,13 +374,14 @@ mod render_entities_world_query_impls { #[inline(always)] unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, ) -> Self::Item<'w, 's> { // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. let component = - unsafe { <&RenderEntity as QueryData>::fetch(fetch, entity, table_row) }; + unsafe { <&RenderEntity as QueryData>::fetch(state, fetch, entity, table_row) }; component.id() } } @@ -397,12 +398,12 @@ mod render_entities_world_query_impls { /// SAFETY: defers completely to `&RenderEntity` implementation, /// and then only modifies the output safely. unsafe impl WorldQuery for MainEntity { - type Fetch<'w, 's> = <&'static MainEntity as WorldQuery>::Fetch<'w, 's>; + type Fetch<'w> = <&'static MainEntity as WorldQuery>::Fetch<'w>; type State = <&'static MainEntity as WorldQuery>::State; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>( + fetch: Self::Fetch<'wlong>, + ) -> Self::Fetch<'wshort> { fetch } @@ -412,7 +413,7 @@ mod render_entities_world_query_impls { component_id: &'s ComponentId, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. unsafe { <&MainEntity as WorldQuery>::init_fetch(world, component_id, last_run, this_run) @@ -423,7 +424,7 @@ mod render_entities_world_query_impls { #[inline] unsafe fn set_archetype<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, component_id: &ComponentId, archetype: &'w Archetype, table: &'w Table, @@ -436,7 +437,7 @@ mod render_entities_world_query_impls { #[inline] unsafe fn set_table<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + fetch: &mut Self::Fetch<'w>, &component_id: &'s ComponentId, table: &'w Table, ) { @@ -482,12 +483,14 @@ mod render_entities_world_query_impls { #[inline(always)] unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, ) -> Self::Item<'w, 's> { // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. - let component = unsafe { <&MainEntity as QueryData>::fetch(fetch, entity, table_row) }; + let component = + unsafe { <&MainEntity as QueryData>::fetch(state, fetch, entity, table_row) }; component.id() } } diff --git a/release-content/migration-guides/query_items_borrow_from_query_state.md b/release-content/migration-guides/query_items_borrow_from_query_state.md index 6c5aff0637..285abb346f 100644 --- a/release-content/migration-guides/query_items_borrow_from_query_state.md +++ b/release-content/migration-guides/query_items_borrow_from_query_state.md @@ -1,9 +1,10 @@ --- title: Query items can borrow from query state -pull_requests: [15396] +pull_requests: [15396, 19720] --- -The `QueryData::Item` and `WorldQuery::Fetch` associated types and the `QueryItem` and `ROQueryItem` type aliases now have an additional lifetime parameter corresponding to the `'s` lifetime in `Query`. +The `QueryData::Item` associated type and the `QueryItem` and `ROQueryItem` type aliases now have an additional lifetime parameter corresponding to the `'s` lifetime in `Query`. +The `QueryData::fetch()` and `QueryFilter::filter_fetch()` methods have a new parameter taking a `&'s WorldQuery::State`. Manual implementations of `WorldQuery` and `QueryData` will need to update the method signatures to include the new lifetimes. Other uses of the types will need to be updated to include a lifetime parameter, although it can usually be passed as `'_`. In particular, `ROQueryItem` is used when implementing `RenderCommand`. From b129764924431ab0a09f0cbcd2860e37695cd120 Mon Sep 17 00:00:00 2001 From: theotherphil Date: Thu, 19 Jun 2025 03:02:39 +0100 Subject: [PATCH 060/106] deny(missing_docs) for bevy_asset_macros (#19719) # Objective More boilerplate docs, towards https://github.com/bevyengine/bevy/issues/3492. Another place where https://github.com/bevyengine/bevy/issues/19537 might be worth doing as a follow up - I've added a comment on that issue. --- crates/bevy_asset/macros/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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); From d2e1f725db05feee42d19f73728fb724db8ff209 Mon Sep 17 00:00:00 2001 From: theotherphil Date: Thu, 19 Jun 2025 20:33:49 +0100 Subject: [PATCH 061/106] deny(missing_docs) for bevy_window (#19655) # Objective Write some more boilerplate-y docs, to get one crate closer to closing https://github.com/bevyengine/bevy/issues/3492. --- crates/bevy_window/src/event.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 5a320439d7..81360ef9c4 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -502,38 +502,66 @@ impl AppLifecycle { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -#[expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] pub enum WindowEvent { + /// An application lifecycle event. AppLifecycle(AppLifecycle), + /// The user's cursor has entered a window. CursorEntered(CursorEntered), + ///The user's cursor has left a window. CursorLeft(CursorLeft), + /// The user's cursor has moved inside a window. CursorMoved(CursorMoved), + /// A file drag and drop event. FileDragAndDrop(FileDragAndDrop), + /// An Input Method Editor event. Ime(Ime), + /// A redraw of all of the application's windows has been requested. RequestRedraw(RequestRedraw), + /// The window's OS-reported scale factor has changed. WindowBackendScaleFactorChanged(WindowBackendScaleFactorChanged), + /// The OS has requested that a window be closed. WindowCloseRequested(WindowCloseRequested), + /// A new window has been created. WindowCreated(WindowCreated), + /// A window has been destroyed by the underlying windowing system. WindowDestroyed(WindowDestroyed), + /// A window has received or lost focus. WindowFocused(WindowFocused), + /// A window has been moved. WindowMoved(WindowMoved), + /// A window has started or stopped being occluded. WindowOccluded(WindowOccluded), + /// A window's logical size has changed. WindowResized(WindowResized), + /// A window's scale factor has changed. WindowScaleFactorChanged(WindowScaleFactorChanged), + /// Sent for windows that are using the system theme when the system theme changes. WindowThemeChanged(WindowThemeChanged), + /// The state of a mouse button has changed. MouseButtonInput(MouseButtonInput), + /// The physical position of a pointing device has changed. MouseMotion(MouseMotion), + /// The mouse wheel has moved. MouseWheel(MouseWheel), + /// A two finger pinch gesture. PinchGesture(PinchGesture), + /// A two finger rotation gesture. RotationGesture(RotationGesture), + /// A double tap gesture. DoubleTapGesture(DoubleTapGesture), + /// A pan gesture. PanGesture(PanGesture), + /// A touch input state change. TouchInput(TouchInput), + /// A keyboard input. KeyboardInput(KeyboardInput), + /// Sent when focus has been lost for all Bevy windows. + /// + /// Used to clear pressed key state. KeyboardFocusLost(KeyboardFocusLost), } From b6bd205e8a5ea5d8424608dc8b74d61a85c7a839 Mon Sep 17 00:00:00 2001 From: theotherphil Date: Thu, 19 Jun 2025 22:58:17 +0100 Subject: [PATCH 062/106] Gate Reflect derive behind feature flag (#19745) # Objective Fix https://github.com/bevyengine/bevy/issues/19733 ## Solution Gate reflect features behind `feature(bevy_reflect)` ## Tests None --- crates/bevy_window/src/window.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index e2a9ca3c0f..403801e9d0 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -1168,13 +1168,17 @@ pub enum MonitorSelection { /// References an exclusive fullscreen video mode. /// /// Used when setting [`WindowMode::Fullscreen`] on a window. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -#[reflect(Debug, PartialEq, Clone)] pub enum VideoModeSelection { /// Uses the video mode that the monitor is already in. Current, From 8e1d0051d2c97dd75c788c874a2b7e652767cc57 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Fri, 20 Jun 2025 08:48:16 -0700 Subject: [PATCH 063/106] Fix QueryData derive codegen (#19750) Custom derived `QueryData` impls currently generate `Item` structs with the lifetimes swapped, which blows up the borrow checker sometimes. See: https://discord.com/channels/691052431525675048/749335865876021248/1385509416086011914 could add a regression test, TBH I don't know the error well enough to do that minimally. Seems like it's that both lifetimes on `QueryData::Item` need to be covariant, but I'm not sure. --- crates/bevy_ecs/macros/src/query_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index 910d9ce3b6..12d9c2bf1c 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -83,7 +83,7 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { let user_generics_with_world_and_state = { let mut generics = ast.generics; generics.params.insert(0, parse_quote!('__w)); - generics.params.insert(0, parse_quote!('__s)); + generics.params.insert(1, parse_quote!('__s)); generics }; let ( From 35166d9029be1ff05c36570e0350b5182488d29b Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Fri, 20 Jun 2025 18:36:08 +0200 Subject: [PATCH 064/106] Refactor bundle derive (#19749) # Objective - Splitted off from #19491 - Make adding generated code to the `Bundle` derive macro easier - Fix a bug when multiple fields are `#[bundle(ignore)]` ## Solution - Instead of accumulating the code for each method in a different `Vec`, accumulate only the names of non-ignored fields and their types, then use `quote` to generate the code for each of them in the method body. - To fix the bug, change the code populating the `BundleFieldKind` to push only one of them per-field (previously each `#[bundle(ignore)]` resulted in pushing twice, once for the correct `BundleFieldKind::Ignore` and then again unconditionally for `BundleFieldKind::Component`) ## Testing - Added a regression test for the bug that was fixed --- crates/bevy_ecs/macros/src/lib.rs | 124 +++++++++++++----------------- crates/bevy_ecs/src/bundle.rs | 9 +++ 2 files changed, 61 insertions(+), 72 deletions(-) diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 8090cff7de..7750f97259 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -16,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, @@ -79,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() @@ -86,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!( @@ -98,7 +100,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { } } - field_kind.push(BundleFieldKind::Component); + field_kind.push(kind); } let field = named_fields @@ -111,82 +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; - let from_components = attributes.impl_from_components.then(|| quote! { - // SAFETY: - // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order - #[allow(deprecated)] - unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause { - #[allow(unused_variables, non_snake_case)] - unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self - where - __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_> - { - Self{ - #(#field_from_components)* - } - } - } - }); - - let attribute_errors = &errors; - - TokenStream::from(quote! { - #(#attribute_errors)* - + 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 @@ -196,27 +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)* + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::register_required_components(components, required_components);)* } } + }; - #from_components - + let dynamic_bundle_impl = quote! { #[allow(deprecated)] impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause { type Effect = (); @@ -226,9 +179,36 @@ 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 }) } diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 2687f7eb16..8efdc60ad9 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -2397,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, + } } From 9fdddf70894de08557d95052dd5686ed8cfd0de0 Mon Sep 17 00:00:00 2001 From: Talin Date: Fri, 20 Jun 2025 09:37:18 -0700 Subject: [PATCH 065/106] Core Checkbox (#19665) # Objective This is part of the "core widgets" effort: https://github.com/bevyengine/bevy/issues/19236. ## Solution This adds the "core checkbox" widget type. ## Testing Tested using examples core_widgets and core_widgets_observers. Note to reviewers: I reorganized the code in the examples, so the diffs are large because of code moves. --- crates/bevy_core_widgets/src/core_checkbox.rs | 179 ++++++ crates/bevy_core_widgets/src/lib.rs | 4 +- crates/bevy_ui/src/interaction_states.rs | 37 +- crates/bevy_ui/src/lib.rs | 8 +- examples/ui/core_widgets.rs | 440 ++++++++++----- examples/ui/core_widgets_observers.rs | 513 +++++++++++++----- .../release-notes/headless-widgets.md | 3 +- 7 files changed, 889 insertions(+), 295 deletions(-) create mode 100644 crates/bevy_core_widgets/src/core_checkbox.rs 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/lib.rs b/crates/bevy_core_widgets/src/lib.rs index 00812bddfc..cdb9142b52 100644 --- a/crates/bevy_core_widgets/src/lib.rs +++ b/crates/bevy_core_widgets/src/lib.rs @@ -15,11 +15,13 @@ // 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, @@ -31,6 +33,6 @@ pub struct CoreWidgetsPlugin; impl Plugin for CoreWidgetsPlugin { fn build(&self, app: &mut App) { - app.add_plugins((CoreButtonPlugin, CoreSliderPlugin)); + app.add_plugins((CoreButtonPlugin, CoreCheckboxPlugin, CoreSliderPlugin)); } } diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs index 6659204da4..b50f4cc245 100644 --- a/crates/bevy_ui/src/interaction_states.rs +++ b/crates/bevy_ui/src/interaction_states.rs @@ -2,7 +2,7 @@ use bevy_a11y::AccessibilityNode; use bevy_ecs::{ component::Component, - lifecycle::{Add, Insert, Remove}, + lifecycle::{Add, Remove}, observer::On, world::DeferredWorld, }; @@ -40,21 +40,17 @@ pub(crate) fn on_remove_disabled( #[derive(Component, Default, Debug)] pub struct Pressed; +/// Component that indicates that a widget can be checked. +#[derive(Component, Default, Debug)] +pub struct Checkable; + /// Component that indicates whether a checkbox or radio button is in a checked state. #[derive(Component, Default, Debug)] -#[component(immutable)] -pub struct Checked(pub bool); +pub struct Checked; -impl Checked { - /// Returns whether the checkbox or radio button is currently checked. - pub fn get(&self) -> bool { - self.0 - } -} - -pub(crate) fn on_insert_is_checked(trigger: On, mut world: DeferredWorld) { +pub(crate) fn on_add_checkable(trigger: On, mut world: DeferredWorld) { let mut entity = world.entity_mut(trigger.target()); - let checked = entity.get::().unwrap().get(); + let checked = entity.get::().is_some(); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_toggled(match checked { true => accesskit::Toggled::True, @@ -63,7 +59,22 @@ pub(crate) fn on_insert_is_checked(trigger: On, mut world: Defe } } -pub(crate) fn on_remove_is_checked(trigger: On, mut world: DeferredWorld) { +pub(crate) fn on_remove_checkable(trigger: On, mut world: DeferredWorld) { + // Remove the 'toggled' attribute entirely. + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.clear_toggled(); + } +} + +pub(crate) fn on_add_checked(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(accesskit::Toggled::True); + } +} + +pub(crate) fn on_remove_checked(trigger: On, mut world: DeferredWorld) { let mut entity = world.entity_mut(trigger.target()); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_toggled(accesskit::Toggled::False); diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index ac70897d06..47d396b201 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -39,7 +39,7 @@ mod ui_node; pub use focus::*; pub use geometry::*; pub use gradients::*; -pub use interaction_states::{Checked, InteractionDisabled, Pressed}; +pub use interaction_states::{Checkable, Checked, InteractionDisabled, Pressed}; pub use layout::*; pub use measurement::*; pub use render::*; @@ -323,8 +323,10 @@ fn build_text_interop(app: &mut App) { app.add_observer(interaction_states::on_add_disabled) .add_observer(interaction_states::on_remove_disabled) - .add_observer(interaction_states::on_insert_is_checked) - .add_observer(interaction_states::on_remove_is_checked); + .add_observer(interaction_states::on_add_checkable) + .add_observer(interaction_states::on_remove_checkable) + .add_observer(interaction_states::on_add_checked) + .add_observer(interaction_states::on_remove_checked); app.configure_sets( PostUpdate, diff --git a/examples/ui/core_widgets.rs b/examples/ui/core_widgets.rs index 27884855fa..fbd4dcd718 100644 --- a/examples/ui/core_widgets.rs +++ b/examples/ui/core_widgets.rs @@ -3,8 +3,8 @@ use bevy::{ color::palettes::basic::*, core_widgets::{ - CoreButton, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue, - TrackClick, + CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, + SliderValue, TrackClick, }, ecs::system::SystemId, input_focus::{ @@ -13,7 +13,7 @@ use bevy::{ }, picking::hover::Hovered, prelude::*, - ui::{InteractionDisabled, Pressed}, + ui::{Checked, InteractionDisabled, Pressed}, winit::WinitSettings, }; @@ -32,6 +32,8 @@ fn main() { update_button_style2, update_slider_style.after(update_widget_values), update_slider_style2.after(update_widget_values), + update_checkbox_style.after(update_widget_values), + update_checkbox_style2.after(update_widget_values), toggle_disabled, ), ) @@ -43,6 +45,8 @@ const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05); const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35); +const CHECKBOX_OUTLINE: Color = Color::srgb(0.45, 0.45, 0.45); +const CHECKBOX_CHECK: Color = Color::srgb(0.35, 0.75, 0.35); /// Marker which identifies buttons with a particular style, in this case the "Demo style". #[derive(Component)] @@ -56,6 +60,10 @@ struct DemoSlider; #[derive(Component, Default)] struct DemoSliderThumb; +/// Marker which identifies checkboxes with a particular style. +#[derive(Component, Default)] +struct DemoCheckbox; + /// A struct to hold the state of various widgets shown in the demo. /// /// While it is possible to use the widget's own state components as the source of truth, @@ -67,128 +75,6 @@ struct DemoWidgetStates { slider_value: f32, } -fn update_button_style( - mut buttons: Query< - ( - Has, - &Hovered, - Has, - &mut BackgroundColor, - &mut BorderColor, - &Children, - ), - ( - Or<( - Changed, - Changed, - Added, - )>, - With, - ), - >, - mut text_query: Query<&mut Text>, -) { - for (pressed, hovered, disabled, mut color, mut border_color, children) in &mut buttons { - let mut text = text_query.get_mut(children[0]).unwrap(); - set_button_style( - disabled, - hovered.get(), - pressed, - &mut color, - &mut border_color, - &mut text, - ); - } -} - -/// Supplementary system to detect removed marker components -fn update_button_style2( - mut buttons: Query< - ( - Has, - &Hovered, - Has, - &mut BackgroundColor, - &mut BorderColor, - &Children, - ), - With, - >, - mut removed_depressed: RemovedComponents, - mut removed_disabled: RemovedComponents, - mut text_query: Query<&mut Text>, -) { - removed_depressed.read().for_each(|entity| { - if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) = - buttons.get_mut(entity) - { - let mut text = text_query.get_mut(children[0]).unwrap(); - set_button_style( - disabled, - hovered.get(), - pressed, - &mut color, - &mut border_color, - &mut text, - ); - } - }); - removed_disabled.read().for_each(|entity| { - if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) = - buttons.get_mut(entity) - { - let mut text = text_query.get_mut(children[0]).unwrap(); - set_button_style( - disabled, - hovered.get(), - pressed, - &mut color, - &mut border_color, - &mut text, - ); - } - }); -} - -fn set_button_style( - disabled: bool, - hovered: bool, - pressed: bool, - color: &mut BackgroundColor, - border_color: &mut BorderColor, - text: &mut Text, -) { - match (disabled, hovered, pressed) { - // Disabled button - (true, _, _) => { - **text = "Disabled".to_string(); - *color = NORMAL_BUTTON.into(); - border_color.set_all(GRAY); - } - - // Pressed and hovered button - (false, true, true) => { - **text = "Press".to_string(); - *color = PRESSED_BUTTON.into(); - border_color.set_all(RED); - } - - // Hovered, unpressed button - (false, true, false) => { - **text = "Hover".to_string(); - *color = HOVERED_BUTTON.into(); - border_color.set_all(WHITE); - } - - // Unhovered button (either pressed or not). - (false, false, _) => { - **text = "Button".to_string(); - *color = NORMAL_BUTTON.into(); - border_color.set_all(BLACK); - } - } -} - /// Update the widget states based on the changing resource. fn update_widget_values( res: Res, @@ -243,6 +129,7 @@ fn demo_root( children![ button(asset_server, on_click), slider(0.0, 100.0, 50.0, Some(on_change_value)), + checkbox(asset_server, "Checkbox", None), Text::new("Press 'D' to toggle widget disabled states"), ], ) @@ -280,6 +167,116 @@ fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { ) } +fn update_button_style( + mut buttons: Query< + ( + Has, + &Hovered, + Has, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + ( + Or<( + Changed, + Changed, + Added, + )>, + With, + ), + >, + mut text_query: Query<&mut Text>, +) { + for (pressed, hovered, disabled, mut color, mut border_color, children) in &mut buttons { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + disabled, + hovered.get(), + pressed, + &mut color, + &mut border_color, + &mut text, + ); + } +} + +/// Supplementary system to detect removed marker components +fn update_button_style2( + mut buttons: Query< + ( + Has, + &Hovered, + Has, + &mut BackgroundColor, + &mut BorderColor, + &Children, + ), + With, + >, + mut removed_depressed: RemovedComponents, + mut removed_disabled: RemovedComponents, + mut text_query: Query<&mut Text>, +) { + removed_depressed + .read() + .chain(removed_disabled.read()) + .for_each(|entity| { + if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) = + buttons.get_mut(entity) + { + let mut text = text_query.get_mut(children[0]).unwrap(); + set_button_style( + disabled, + hovered.get(), + pressed, + &mut color, + &mut border_color, + &mut text, + ); + } + }); +} + +fn set_button_style( + disabled: bool, + hovered: bool, + pressed: bool, + color: &mut BackgroundColor, + border_color: &mut BorderColor, + text: &mut Text, +) { + match (disabled, hovered, pressed) { + // Disabled button + (true, _, _) => { + **text = "Disabled".to_string(); + *color = NORMAL_BUTTON.into(); + border_color.set_all(GRAY); + } + + // Pressed and hovered button + (false, true, true) => { + **text = "Press".to_string(); + *color = PRESSED_BUTTON.into(); + border_color.set_all(RED); + } + + // Hovered, unpressed button + (false, true, false) => { + **text = "Hover".to_string(); + *color = HOVERED_BUTTON.into(); + border_color.set_all(WHITE); + } + + // Unhovered button (either pressed or not). + (false, false, _) => { + **text = "Button".to_string(); + *color = NORMAL_BUTTON.into(); + border_color.set_all(BLACK); + } + } +} + /// Create a demo slider fn slider(min: f32, max: f32, value: f32, on_change: Option>>) -> impl Bundle { ( @@ -412,21 +409,208 @@ fn thumb_color(disabled: bool, hovered: bool) -> Color { } } +/// Create a demo checkbox +fn checkbox( + asset_server: &AssetServer, + caption: &str, + on_change: Option>>, +) -> impl Bundle { + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::FlexStart, + align_items: AlignItems::Center, + align_content: AlignContent::Center, + column_gap: Val::Px(4.0), + ..default() + }, + Name::new("Checkbox"), + Hovered::default(), + DemoCheckbox, + CoreCheckbox { on_change }, + TabIndex(0), + Children::spawn(( + Spawn(( + // Checkbox outer + Node { + display: Display::Flex, + width: Val::Px(16.0), + height: Val::Px(16.0), + border: UiRect::all(Val::Px(2.0)), + ..default() + }, + BorderColor::all(CHECKBOX_OUTLINE), // Border color for the checkbox + BorderRadius::all(Val::Px(3.0)), + children![ + // Checkbox inner + ( + Node { + display: Display::Flex, + width: Val::Px(8.0), + height: Val::Px(8.0), + position_type: PositionType::Absolute, + left: Val::Px(2.0), + top: Val::Px(2.0), + ..default() + }, + BackgroundColor(CHECKBOX_CHECK), + ), + ], + )), + Spawn(( + Text::new(caption), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 20.0, + ..default() + }, + )), + )), + ) +} + +// Update the checkbox's styles. +fn update_checkbox_style( + mut q_checkbox: Query< + (Has, &Hovered, Has, &Children), + ( + With, + Or<( + Added, + Changed, + Added, + Added, + )>, + ), + >, + mut q_border_color: Query<(&mut BorderColor, &mut Children), Without>, + mut q_bg_color: Query<&mut BackgroundColor, (Without, Without)>, +) { + for (checked, Hovered(is_hovering), is_disabled, children) in q_checkbox.iter_mut() { + let Some(border_id) = children.first() else { + continue; + }; + + let Ok((mut border_color, border_children)) = q_border_color.get_mut(*border_id) else { + continue; + }; + + let Some(mark_id) = border_children.first() else { + warn!("Checkbox does not have a mark entity."); + continue; + }; + + let Ok(mut mark_bg) = q_bg_color.get_mut(*mark_id) else { + warn!("Checkbox mark entity lacking a background color."); + continue; + }; + + set_checkbox_style( + is_disabled, + *is_hovering, + checked, + &mut border_color, + &mut mark_bg, + ); + } +} + +fn update_checkbox_style2( + mut q_checkbox: Query< + (Has, &Hovered, Has, &Children), + With, + >, + mut q_border_color: Query<(&mut BorderColor, &mut Children), Without>, + mut q_bg_color: Query<&mut BackgroundColor, (Without, Without)>, + mut removed_checked: RemovedComponents, + mut removed_disabled: RemovedComponents, +) { + removed_checked + .read() + .chain(removed_disabled.read()) + .for_each(|entity| { + if let Ok((checked, Hovered(is_hovering), is_disabled, children)) = + q_checkbox.get_mut(entity) + { + let Some(border_id) = children.first() else { + return; + }; + + let Ok((mut border_color, border_children)) = q_border_color.get_mut(*border_id) + else { + return; + }; + + let Some(mark_id) = border_children.first() else { + warn!("Checkbox does not have a mark entity."); + return; + }; + + let Ok(mut mark_bg) = q_bg_color.get_mut(*mark_id) else { + warn!("Checkbox mark entity lacking a background color."); + return; + }; + + set_checkbox_style( + is_disabled, + *is_hovering, + checked, + &mut border_color, + &mut mark_bg, + ); + } + }); +} + +fn set_checkbox_style( + disabled: bool, + hovering: bool, + checked: bool, + border_color: &mut BorderColor, + mark_bg: &mut BackgroundColor, +) { + let color: Color = if disabled { + // If the checkbox is disabled, use a lighter color + CHECKBOX_OUTLINE.with_alpha(0.2) + } else if hovering { + // If hovering, use a lighter color + CHECKBOX_OUTLINE.lighter(0.2) + } else { + // Default color for the checkbox + CHECKBOX_OUTLINE + }; + + // Update the background color of the check mark + border_color.set_all(color); + + let mark_color: Color = match (disabled, checked) { + (true, true) => CHECKBOX_CHECK.with_alpha(0.5), + (false, true) => CHECKBOX_CHECK, + (_, false) => Srgba::NONE.into(), + }; + + if mark_bg.0 != mark_color { + // Update the color of the check mark + mark_bg.0 = mark_color; + } +} + fn toggle_disabled( input: Res>, mut interaction_query: Query< (Entity, Has), - Or<(With, With)>, + Or<(With, With, With)>, >, mut commands: Commands, ) { if input.just_pressed(KeyCode::KeyD) { for (entity, disabled) in &mut interaction_query { if disabled { - info!("Widgets enabled"); + info!("Widget enabled"); commands.entity(entity).remove::(); } else { - info!("Widgets disabled"); + info!("Widget disabled"); commands.entity(entity).insert(InteractionDisabled); } } diff --git a/examples/ui/core_widgets_observers.rs b/examples/ui/core_widgets_observers.rs index c2f7315ba9..c3451fe700 100644 --- a/examples/ui/core_widgets_observers.rs +++ b/examples/ui/core_widgets_observers.rs @@ -3,7 +3,8 @@ use bevy::{ color::palettes::basic::*, core_widgets::{ - CoreButton, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue, + CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, + SliderValue, }, ecs::system::SystemId, input_focus::{ @@ -12,7 +13,7 @@ use bevy::{ }, picking::hover::Hovered, prelude::*, - ui::{InteractionDisabled, Pressed}, + ui::{Checked, InteractionDisabled, Pressed}, winit::WinitSettings, }; @@ -33,6 +34,11 @@ fn main() { .add_observer(slider_on_change_hover) .add_observer(slider_on_change_value) .add_observer(slider_on_change_range) + .add_observer(checkbox_on_add_disabled) + .add_observer(checkbox_on_remove_disabled) + .add_observer(checkbox_on_change_hover) + .add_observer(checkbox_on_add_checked) + .add_observer(checkbox_on_remove_checked) .add_systems(Update, (update_widget_values, toggle_disabled)) .run(); } @@ -42,6 +48,8 @@ const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05); const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35); +const CHECKBOX_OUTLINE: Color = Color::srgb(0.45, 0.45, 0.45); +const CHECKBOX_CHECK: Color = Color::srgb(0.35, 0.75, 0.35); /// Marker which identifies buttons with a particular style, in this case the "Demo style". #[derive(Component)] @@ -55,6 +63,10 @@ struct DemoSlider; #[derive(Component, Default)] struct DemoSliderThumb; +/// Marker which identifies checkboxes with a particular style. +#[derive(Component, Default)] +struct DemoCheckbox; + /// A struct to hold the state of various widgets shown in the demo. /// /// While it is possible to use the widget's own state components as the source of truth, @@ -66,6 +78,83 @@ struct DemoWidgetStates { slider_value: f32, } +fn setup(mut commands: Commands, assets: Res) { + // System to print a value when the button is clicked. + let on_click = commands.register_system(|| { + info!("Button clicked!"); + }); + + // System to update a resource when the slider value changes. Note that we could have + // updated the slider value directly, but we want to demonstrate externalizing the state. + let on_change_value = commands.register_system( + |value: In, mut widget_states: ResMut| { + widget_states.slider_value = *value; + }, + ); + + // ui camera + commands.spawn(Camera2d); + commands.spawn(demo_root(&assets, on_click, on_change_value)); +} + +fn demo_root( + asset_server: &AssetServer, + on_click: SystemId, + on_change_value: SystemId>, +) -> impl Bundle { + ( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + display: Display::Flex, + flex_direction: FlexDirection::Column, + row_gap: Val::Px(10.0), + ..default() + }, + TabGroup::default(), + children![ + button(asset_server, on_click), + slider(0.0, 100.0, 50.0, Some(on_change_value)), + checkbox(asset_server, "Checkbox", None), + Text::new("Press 'D' to toggle widget disabled states"), + ], + ) +} + +fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { + ( + Node { + width: Val::Px(150.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + DemoButton, + CoreButton { + on_click: Some(on_click), + }, + Hovered::default(), + TabIndex(0), + BorderColor::all(Color::BLACK), + BorderRadius::MAX, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Button"), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + TextShadow::default(), + )], + ) +} + fn button_on_add_pressed( trigger: On, mut buttons: Query< @@ -256,6 +345,74 @@ fn set_button_style( } } +/// Create a demo slider +fn slider(min: f32, max: f32, value: f32, on_change: Option>>) -> impl Bundle { + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Stretch, + justify_items: JustifyItems::Center, + column_gap: Val::Px(4.0), + height: Val::Px(12.0), + width: Val::Percent(30.0), + ..default() + }, + Name::new("Slider"), + Hovered::default(), + DemoSlider, + CoreSlider { + on_change, + ..default() + }, + SliderValue(value), + SliderRange::new(min, max), + TabIndex(0), + Children::spawn(( + // Slider background rail + Spawn(( + Node { + height: Val::Px(6.0), + ..default() + }, + BackgroundColor(SLIDER_TRACK), // Border color for the checkbox + BorderRadius::all(Val::Px(3.0)), + )), + // Invisible track to allow absolute placement of thumb entity. This is narrower than + // the actual slider, which allows us to position the thumb entity using simple + // percentages, without having to measure the actual width of the slider thumb. + Spawn(( + Node { + display: Display::Flex, + position_type: PositionType::Absolute, + left: Val::Px(0.0), + // Track is short by 12px to accommodate the thumb. + right: Val::Px(12.0), + top: Val::Px(0.0), + bottom: Val::Px(0.0), + ..default() + }, + children![( + // Thumb + DemoSliderThumb, + CoreSliderThumb, + Node { + display: Display::Flex, + width: Val::Px(12.0), + height: Val::Px(12.0), + position_type: PositionType::Absolute, + left: Val::Percent(0.0), // This will be updated by the slider's value + ..default() + }, + BorderRadius::MAX, + BackgroundColor(SLIDER_THUMB), + )], + )), + )), + ) +} + fn slider_on_add_disabled( trigger: On, sliders: Query<(Entity, &Hovered), With>, @@ -351,6 +508,208 @@ fn thumb_color(disabled: bool, hovered: bool) -> Color { } } +/// Create a demo checkbox +fn checkbox( + asset_server: &AssetServer, + caption: &str, + on_change: Option>>, +) -> impl Bundle { + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::FlexStart, + align_items: AlignItems::Center, + align_content: AlignContent::Center, + column_gap: Val::Px(4.0), + ..default() + }, + Name::new("Checkbox"), + Hovered::default(), + DemoCheckbox, + CoreCheckbox { on_change }, + TabIndex(0), + Children::spawn(( + Spawn(( + // Checkbox outer + Node { + display: Display::Flex, + width: Val::Px(16.0), + height: Val::Px(16.0), + border: UiRect::all(Val::Px(2.0)), + ..default() + }, + BorderColor::all(CHECKBOX_OUTLINE), // Border color for the checkbox + BorderRadius::all(Val::Px(3.0)), + children![ + // Checkbox inner + ( + Node { + display: Display::Flex, + width: Val::Px(8.0), + height: Val::Px(8.0), + position_type: PositionType::Absolute, + left: Val::Px(2.0), + top: Val::Px(2.0), + ..default() + }, + BackgroundColor(Srgba::NONE.into()), + ), + ], + )), + Spawn(( + Text::new(caption), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 20.0, + ..default() + }, + )), + )), + ) +} + +fn checkbox_on_add_disabled( + trigger: On, + checkboxes: Query<(&Hovered, Has, &Children), With>, + mut borders: Query<(&mut BorderColor, &mut Children), Without>, + mut marks: Query<&mut BackgroundColor, (Without, Without)>, +) { + if let Ok((hovered, checked, children)) = checkboxes.get(trigger.target()) { + set_checkbox_style(children, &mut borders, &mut marks, true, hovered.0, checked); + } +} + +fn checkbox_on_remove_disabled( + trigger: On, + checkboxes: Query<(&Hovered, Has, &Children), With>, + mut borders: Query<(&mut BorderColor, &mut Children), Without>, + mut marks: Query<&mut BackgroundColor, (Without, Without)>, +) { + if let Ok((hovered, checked, children)) = checkboxes.get(trigger.target()) { + set_checkbox_style( + children, + &mut borders, + &mut marks, + false, + hovered.0, + checked, + ); + } +} + +fn checkbox_on_change_hover( + trigger: On, + checkboxes: Query< + (&Hovered, Has, Has, &Children), + With, + >, + mut borders: Query<(&mut BorderColor, &mut Children), Without>, + mut marks: Query<&mut BackgroundColor, (Without, Without)>, +) { + if let Ok((hovered, disabled, checked, children)) = checkboxes.get(trigger.target()) { + set_checkbox_style( + children, + &mut borders, + &mut marks, + disabled, + hovered.0, + checked, + ); + } +} + +fn checkbox_on_add_checked( + trigger: On, + checkboxes: Query< + (&Hovered, Has, Has, &Children), + With, + >, + mut borders: Query<(&mut BorderColor, &mut Children), Without>, + mut marks: Query<&mut BackgroundColor, (Without, Without)>, +) { + if let Ok((hovered, disabled, checked, children)) = checkboxes.get(trigger.target()) { + set_checkbox_style( + children, + &mut borders, + &mut marks, + disabled, + hovered.0, + checked, + ); + } +} + +fn checkbox_on_remove_checked( + trigger: On, + checkboxes: Query<(&Hovered, Has, &Children), With>, + mut borders: Query<(&mut BorderColor, &mut Children), Without>, + mut marks: Query<&mut BackgroundColor, (Without, Without)>, +) { + if let Ok((hovered, disabled, children)) = checkboxes.get(trigger.target()) { + set_checkbox_style( + children, + &mut borders, + &mut marks, + disabled, + hovered.0, + false, + ); + } +} + +fn set_checkbox_style( + children: &Children, + borders: &mut Query<(&mut BorderColor, &mut Children), Without>, + marks: &mut Query<&mut BackgroundColor, (Without, Without)>, + disabled: bool, + hovering: bool, + checked: bool, +) { + let Some(border_id) = children.first() else { + return; + }; + + let Ok((mut border_color, border_children)) = borders.get_mut(*border_id) else { + return; + }; + + let Some(mark_id) = border_children.first() else { + warn!("Checkbox does not have a mark entity."); + return; + }; + + let Ok(mut mark_bg) = marks.get_mut(*mark_id) else { + warn!("Checkbox mark entity lacking a background color."); + return; + }; + + let color: Color = if disabled { + // If the checkbox is disabled, use a lighter color + CHECKBOX_OUTLINE.with_alpha(0.2) + } else if hovering { + // If hovering, use a lighter color + CHECKBOX_OUTLINE.lighter(0.2) + } else { + // Default color for the checkbox + CHECKBOX_OUTLINE + }; + + // Update the background color of the check mark + border_color.set_all(color); + + let mark_color: Color = match (disabled, checked) { + (true, true) => CHECKBOX_CHECK.with_alpha(0.5), + (false, true) => CHECKBOX_CHECK, + (_, false) => Srgba::NONE.into(), + }; + + if mark_bg.0 != mark_color { + // Update the color of the check mark + mark_bg.0 = mark_color; + } +} + /// Update the widget states based on the changing resource. fn update_widget_values( res: Res, @@ -366,165 +725,21 @@ fn update_widget_values( } } -fn setup(mut commands: Commands, assets: Res) { - // System to print a value when the button is clicked. - let on_click = commands.register_system(|| { - info!("Button clicked!"); - }); - - // System to update a resource when the slider value changes. Note that we could have - // updated the slider value directly, but we want to demonstrate externalizing the state. - let on_change_value = commands.register_system( - |value: In, mut widget_states: ResMut| { - widget_states.slider_value = *value; - }, - ); - - // ui camera - commands.spawn(Camera2d); - commands.spawn(demo_root(&assets, on_click, on_change_value)); -} - -fn demo_root( - asset_server: &AssetServer, - on_click: SystemId, - on_change_value: SystemId>, -) -> impl Bundle { - ( - Node { - width: Val::Percent(100.0), - height: Val::Percent(100.0), - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - display: Display::Flex, - flex_direction: FlexDirection::Column, - row_gap: Val::Px(10.0), - ..default() - }, - TabGroup::default(), - children![ - button(asset_server, on_click), - slider(0.0, 100.0, 50.0, Some(on_change_value)), - Text::new("Press 'D' to toggle widget disabled states"), - ], - ) -} - -fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { - ( - Node { - width: Val::Px(150.0), - height: Val::Px(65.0), - border: UiRect::all(Val::Px(5.0)), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }, - DemoButton, - CoreButton { - on_click: Some(on_click), - }, - Hovered::default(), - TabIndex(0), - BorderColor::all(Color::BLACK), - BorderRadius::MAX, - BackgroundColor(NORMAL_BUTTON), - children![( - Text::new("Button"), - TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 33.0, - ..default() - }, - TextColor(Color::srgb(0.9, 0.9, 0.9)), - TextShadow::default(), - )], - ) -} - -/// Create a demo slider -fn slider(min: f32, max: f32, value: f32, on_change: Option>>) -> impl Bundle { - ( - Node { - display: Display::Flex, - flex_direction: FlexDirection::Column, - justify_content: JustifyContent::Center, - align_items: AlignItems::Stretch, - justify_items: JustifyItems::Center, - column_gap: Val::Px(4.0), - height: Val::Px(12.0), - width: Val::Percent(30.0), - ..default() - }, - Name::new("Slider"), - Hovered::default(), - DemoSlider, - CoreSlider { - on_change, - ..default() - }, - SliderValue(value), - SliderRange::new(min, max), - TabIndex(0), - Children::spawn(( - // Slider background rail - Spawn(( - Node { - height: Val::Px(6.0), - ..default() - }, - BackgroundColor(SLIDER_TRACK), // Border color for the checkbox - BorderRadius::all(Val::Px(3.0)), - )), - // Invisible track to allow absolute placement of thumb entity. This is narrower than - // the actual slider, which allows us to position the thumb entity using simple - // percentages, without having to measure the actual width of the slider thumb. - Spawn(( - Node { - display: Display::Flex, - position_type: PositionType::Absolute, - left: Val::Px(0.0), - // Track is short by 12px to accommodate the thumb. - right: Val::Px(12.0), - top: Val::Px(0.0), - bottom: Val::Px(0.0), - ..default() - }, - children![( - // Thumb - DemoSliderThumb, - CoreSliderThumb, - Node { - display: Display::Flex, - width: Val::Px(12.0), - height: Val::Px(12.0), - position_type: PositionType::Absolute, - left: Val::Percent(0.0), // This will be updated by the slider's value - ..default() - }, - BorderRadius::MAX, - BackgroundColor(SLIDER_THUMB), - )], - )), - )), - ) -} - fn toggle_disabled( input: Res>, mut interaction_query: Query< (Entity, Has), - Or<(With, With)>, + Or<(With, With, With)>, >, mut commands: Commands, ) { if input.just_pressed(KeyCode::KeyD) { for (entity, disabled) in &mut interaction_query { if disabled { - info!("Widgets enabled"); + info!("Widget enabled"); commands.entity(entity).remove::(); } else { - info!("Widgets disabled"); + info!("Widget disabled"); commands.entity(entity).insert(InteractionDisabled); } } diff --git a/release-content/release-notes/headless-widgets.md b/release-content/release-notes/headless-widgets.md index b65cae4119..6fc82648cc 100644 --- a/release-content/release-notes/headless-widgets.md +++ b/release-content/release-notes/headless-widgets.md @@ -1,7 +1,7 @@ --- title: Headless Widgets authors: ["@viridia"] -pull_requests: [19366, 19584] +pull_requests: [19366, 19584, 19665] --- Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately @@ -34,6 +34,7 @@ sliders, checkboxes and radio buttons. - `CoreButton` is a push button. It emits an activation event when clicked. - `CoreSlider` is a standard slider, which lets you edit an `f32` value in a given range. +- `CoreCheckbox` can be used for checkboxes and toggle switches. ## Widget Interaction States From ffd6c9e1c93c3d96ae2a951ef924aca87a4f43fa Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Fri, 20 Jun 2025 18:43:14 +0200 Subject: [PATCH 066/106] Rewrite camera shake example (#19724) # Objective - Alternative to #19721 - The old implementation had several issues: - underexplained - bit complicated in places - did not follow the source as described - camera moves away - camera does not use noise - camera nudges back after shake ends, which looks cinematic, but not what you want in practice All in all: the old implementation did not really show a typical implementation IMO ## Solution - Rewrite it :D - I believe the current implementation is a robust thing you can learn from or just copy-paste into your project ## Testing https://github.com/user-attachments/assets/bfe74fb6-c428-4d5a-9c9c-cd4a034ba176 --------- Co-authored-by: Rob Parrett --- examples/camera/2d_screen_shake.rs | 376 +++++++++++++++++++---------- 1 file changed, 250 insertions(+), 126 deletions(-) diff --git a/examples/camera/2d_screen_shake.rs b/examples/camera/2d_screen_shake.rs index dcdcd68811..0a0aaa780d 100644 --- a/examples/camera/2d_screen_shake.rs +++ b/examples/camera/2d_screen_shake.rs @@ -1,70 +1,216 @@ -//! This example showcases a 2D screen shake using concept in this video: `` +//! This example showcases how to implement 2D screen shake. +//! It follows the GDC talk ["Math for Game Programmers: Juicing Your Cameras With Math"](https://www.youtube.com/watch?v=tu-Qe66AvtY) by Squirrel Eiserloh +//! +//! The key features are: +//! - Camera shake is dependent on a "trauma" value between 0.0 and 1.0. The more trauma, the stronger the shake. +//! - Trauma automatically decays over time. +//! - The camera shake will always only affect the camera `Transform` up to a maximum displacement. +//! - The camera's `Transform` is only affected by the shake for the rendering. The `Transform` stays "normal" for the rest of the game logic. +//! - All displacements are governed by a noise function, guaranteeing that the shake is smooth and continuous. +//! This means that the camera won't jump around wildly. //! //! ## Controls //! -//! | Key Binding | Action | -//! |:-------------|:---------------------| -//! | Space | Trigger screen shake | +//! | Key Binding | Action | +//! |:---------------------------------|:---------------------------| +//! | Space (pressed repeatedly) | Increase camera trauma | -use bevy::{prelude::*, render::camera::SubCameraView, sprite::MeshMaterial2d}; -use rand::{Rng, SeedableRng}; -use rand_chacha::ChaCha8Rng; +use bevy::{ + input::common_conditions::input_just_pressed, math::ops::powf, prelude::*, + sprite::MeshMaterial2d, +}; -const CAMERA_DECAY_RATE: f32 = 0.9; // Adjust this for smoother or snappier decay -const TRAUMA_DECAY_SPEED: f32 = 0.5; // How fast trauma decays -const TRAUMA_INCREMENT: f32 = 1.0; // Increment of trauma per frame when holding space +// Before we implement the code, let's quickly introduce the underlying constants. +// They are later encoded in a `CameraShakeConfig` component, but introduced here so we can easily tweak them. +// Try playing around with them and see how the shake behaves! -// screen_shake parameters, maximum addition by frame not actual maximum overall values -const MAX_ANGLE: f32 = 0.5; -const MAX_OFFSET: f32 = 500.0; +/// The trauma decay rate controls how quickly the trauma decays. +/// 0.5 means that a full trauma of 1.0 will decay to 0.0 in 2 seconds. +const TRAUMA_DECAY_PER_SECOND: f32 = 0.5; -#[derive(Component)] -struct Player; +/// The trauma exponent controls how the trauma affects the shake. +/// Camera shakes don't feel punchy when they go up linearly, so we use an exponent of 2.0. +/// The higher the exponent, the more abrupt is the transition between no shake and full shake. +const TRAUMA_EXPONENT: f32 = 2.0; + +/// The maximum angle the camera can rotate on full trauma. +/// 10.0 degrees is a somewhat high but still reasonable shake. Try bigger values for something more silly and wiggly. +const MAX_ANGLE: f32 = 10.0_f32.to_radians(); + +/// The maximum translation the camera will move on full trauma in both the x and y directions. +/// 20.0 px is a low enough displacement to not be distracting. Try higher values for an effect that looks like the camera is wandering around. +const MAX_TRANSLATION: f32 = 20.0; + +/// How much we are traversing the noise function in arbitrary units per second. +/// This dictates how fast the camera shakes. +/// 20.0 is a fairly fast shake. Try lower values for a more dreamy effect. +const NOISE_SPEED: f32 = 20.0; + +/// How much trauma we add per press of the space key. +/// A value of 1.0 would mean that a single press would result in a maximum trauma, i.e. 1.0. +const TRAUMA_PER_PRESS: f32 = 0.4; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, (setup_scene, setup_instructions, setup_camera)) - .add_systems(Update, (screen_shake, trigger_shake_on_space)) + // At the start of the frame, restore the camera's transform to its unshaken state. + .add_systems(PreUpdate, reset_transform) + .add_systems( + Update, + // Increase trauma when the space key is pressed. + increase_trauma.run_if(input_just_pressed(KeyCode::Space)), + ) + // Just before the end of the frame, apply the shake. + // This is ordered so that the transform propagation produces correct values for the global transform, which is used by Bevy's rendering. + .add_systems(PostUpdate, shake_camera.before(TransformSystems::Propagate)) .run(); } +/// Let's start with the core mechanic: how do we shake the camera? +/// This system runs right at the end of the frame, so that we can sneak in the shake effect before rendering kicks in. +fn shake_camera( + camera_shake: Single<(&mut CameraShakeState, &CameraShakeConfig, &mut Transform)>, + time: Res(); @@ -1915,9 +1913,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&mut bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_immut_to_mut() { let mut world = World::new(); world.spawn(A(0)); @@ -1927,9 +1923,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (core::option::Option<&bevy_ecs::query::state::tests::A>, ())." - )] + #[should_panic] fn cannot_transmute_option_to_immut() { let mut world = World::new(); world.spawn(C(0)); @@ -1941,9 +1935,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (bevy_ecs::world::entity_ref::EntityRef, ())." - )] + #[should_panic] fn cannot_transmute_entity_ref() { let mut world = World::new(); world.register_component::(); @@ -2009,9 +2001,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (bevy_ecs::entity::Entity, bevy_ecs::query::filter::Changed) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_changed_without_access() { let mut world = World::new(); world.register_component::(); @@ -2021,9 +2011,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&mut bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_mutable_after_readonly() { let mut world = World::new(); // Calling this method would mean we had aliasing queries. @@ -2130,9 +2118,7 @@ mod tests { } #[test] - #[should_panic(expected = "Joined state for (&bevy_ecs::query::state::tests::C, ()) \ - attempts to access terms that are not allowed by state \ - (&bevy_ecs::query::state::tests::A, ()) joined with (&bevy_ecs::query::state::tests::B, ()).")] + #[should_panic] fn cannot_join_wrong_fetch() { let mut world = World::new(); world.register_component::(); @@ -2142,12 +2128,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Joined state for (bevy_ecs::entity::Entity, bevy_ecs::query::filter::Changed) \ - attempts to access terms that are not allowed by state \ - (&bevy_ecs::query::state::tests::A, bevy_ecs::query::filter::Without) \ - joined with (&bevy_ecs::query::state::tests::B, bevy_ecs::query::filter::Without)." - )] + #[should_panic] fn cannot_join_wrong_filter() { let mut world = World::new(); let query_1 = QueryState::<&A, Without>::new(&mut world); @@ -2156,9 +2137,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Joined state for ((&mut bevy_ecs::query::state::tests::A, &mut bevy_ecs::query::state::tests::B), ()) attempts to access terms that are not allowed by state (&bevy_ecs::query::state::tests::A, ()) joined with (&mut bevy_ecs::query::state::tests::B, ())." - )] + #[should_panic] fn cannot_join_mutable_after_readonly() { let mut world = World::new(); // Calling this method would mean we had aliasing queries. diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 91f1b41312..12f58a7cd3 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -26,7 +26,9 @@ pub mod passes { #[cfg(test)] mod tests { use super::*; - use alloc::{string::ToString, vec, vec::Vec}; + #[cfg(feature = "trace")] + use alloc::string::ToString; + use alloc::{vec, vec::Vec}; use core::sync::atomic::{AtomicU32, Ordering}; use crate::error::BevyError; @@ -770,6 +772,7 @@ mod tests { } mod system_ambiguity { + #[cfg(feature = "trace")] use alloc::collections::BTreeSet; use super::*; @@ -1110,6 +1113,7 @@ mod tests { // Tests that the correct ambiguities were reported in the correct order. #[test] + #[cfg(feature = "trace")] fn correct_ambiguities() { fn system_a(_res: ResMut) {} fn system_b(_res: ResMut) {} @@ -1183,6 +1187,7 @@ mod tests { // Test that anonymous set names work properly // Related issue https://github.com/bevyengine/bevy/issues/9641 #[test] + #[cfg(feature = "trace")] fn anonymous_set_name() { let mut schedule = Schedule::new(TestSchedule); schedule.add_systems((resmut_system, resmut_system).run_if(|| true)); diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index db4cd452c0..3e63dcf3d6 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -634,7 +634,7 @@ mod tests { } #[test] - #[should_panic = "&bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_mut_and_ref() { fn sys(_: Query>) {} let mut world = World::default(); @@ -642,7 +642,7 @@ mod tests { } #[test] - #[should_panic = "&mut bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_ref_and_mut() { fn sys(_: Query>) {} let mut world = World::default(); @@ -650,7 +650,7 @@ mod tests { } #[test] - #[should_panic = "&bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_mut_and_option() { fn sys(_: Query)>>) {} let mut world = World::default(); @@ -680,7 +680,7 @@ mod tests { } #[test] - #[should_panic = "&mut bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_conflicting() { fn sys(_: Query>) {} let mut world = World::default(); @@ -1629,54 +1629,42 @@ mod tests { } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_world_and_entity_mut_system_does_conflict_first::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" - )] + #[should_panic] fn assert_world_and_entity_mut_system_does_conflict_first() { fn system(_query: &World, _q2: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "&World conflicts with a previous mutable system parameter. Allowing this would break Rust's mutability rules" - )] + #[should_panic] fn assert_world_and_entity_mut_system_does_conflict_second() { fn system(_: Query, _: &World) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_ref_and_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" - )] + #[should_panic] fn assert_entity_ref_and_entity_mut_system_does_conflict() { fn system(_query: Query, _q2: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" - )] + #[should_panic] fn assert_entity_mut_system_does_conflict() { fn system(_query: Query, _q2: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_deferred_world_and_entity_ref_system_does_conflict_first::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" - )] + #[should_panic] fn assert_deferred_world_and_entity_ref_system_does_conflict_first() { fn system(_world: DeferredWorld, _query: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "DeferredWorld in system bevy_ecs::system::tests::assert_deferred_world_and_entity_ref_system_does_conflict_second::system conflicts with a previous access." - )] + #[should_panic] fn assert_deferred_world_and_entity_ref_system_does_conflict_second() { fn system(_query: Query, _world: DeferredWorld) {} super::assert_system_does_not_conflict(system); diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 8527f3d3b8..d4521e76f5 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -410,7 +410,6 @@ pub enum RunSystemError { mod tests { use super::*; use crate::prelude::*; - use alloc::string::ToString; #[test] fn run_system_once() { @@ -483,7 +482,5 @@ mod tests { let result = world.run_system_once(system); assert!(matches!(result, Err(RunSystemError::InvalidParams { .. }))); - let expected = "System bevy_ecs::system::system::tests::run_system_once_invalid_params::system did not run due to failed parameter validation: Parameter `Res` failed validation: Resource does not exist\nIf this is an expected state, wrap the parameter in `Option` and handle `None` when it happens, or wrap the parameter in `When` to skip the system when it happens."; - assert_eq!(expected, result.unwrap_err().to_string()); } } diff --git a/crates/bevy_ecs/src/system/system_name.rs b/crates/bevy_ecs/src/system/system_name.rs index f38a5eb1aa..e0c3c952cf 100644 --- a/crates/bevy_ecs/src/system/system_name.rs +++ b/crates/bevy_ecs/src/system/system_name.rs @@ -85,6 +85,7 @@ impl ExclusiveSystemParam for SystemName { } #[cfg(test)] +#[cfg(feature = "trace")] mod tests { use crate::{ system::{IntoSystem, RunSystemOnce, SystemName}, diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 65fecaf564..8faa6f1b0c 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -151,6 +151,7 @@ use variadics_please::{all_tuples, all_tuples_enumerated}; /// let mut world = World::new(); /// let err = world.run_system_cached(|param: MyParam| {}).unwrap_err(); /// let expected = "Parameter `MyParam::foo` failed validation: Custom Message"; +/// # #[cfg(feature="Trace")] // Without debug_utils/debug enabled MyParam::foo is stripped and breaks the assert /// assert!(err.to_string().contains(expected)); /// ``` /// @@ -3076,7 +3077,7 @@ mod tests { } #[test] - #[should_panic = "Encountered an error in system `bevy_ecs::system::system_param::tests::missing_resource_error::res_system`: Parameter `Res` failed validation: Resource does not exist"] + #[should_panic] fn missing_resource_error() { #[derive(Resource)] pub struct MissingResource; @@ -3090,7 +3091,7 @@ mod tests { } #[test] - #[should_panic = "Encountered an error in system `bevy_ecs::system::system_param::tests::missing_event_error::event_system`: Parameter `EventReader::events` failed validation: BufferedEvent not initialized"] + #[should_panic] fn missing_event_error() { use crate::prelude::{BufferedEvent, EventReader}; diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index 6889a8f4cd..e9c9cdba13 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -913,7 +913,6 @@ mod tests { #[test] fn run_system_invalid_params() { use crate::system::RegisteredSystemError; - use alloc::{format, string::ToString}; struct T; impl Resource for T {} @@ -928,8 +927,6 @@ mod tests { result, Err(RegisteredSystemError::InvalidParams { .. }) )); - let expected = format!("System {id:?} did not run due to failed parameter validation: Parameter `Res` failed validation: Resource does not exist\nIf this is an expected state, wrap the parameter in `Option` and handle `None` when it happens, or wrap the parameter in `When` to skip the system when it happens."); - assert_eq!(expected, result.unwrap_err().to_string()); } #[test] From 45a3f3d138cdd599bf007490f8f8146920d9c910 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sat, 21 Jun 2025 16:06:35 +0100 Subject: [PATCH 071/106] Color interpolation in OKLab, OKLCH spaces for UI gradients (#19330) # Objective Add support for interpolation in OKLab and OKLCH color spaces for UI gradients. ## Solution * New `InterpolationColorSpace` enum with `OkLab`, `OkLch`, `OkLchLong`, `Srgb` and `LinearRgb` variants. * Added a color space specialization to the gradients pipeline. * Added support for interpolation in OkLCH and OkLAB color spaces to the gradients shader. OKLCH interpolation supports both short and long hue paths. This is mostly based on the conversion functions from `bevy_color` except that interpolation in polar space uses radians. * Added `color_space` fields to each gradient type. ## Testing The `gradients` example has been updated to demonstrate the different color interpolation methods. Press space to cycle through the different options. --- ## Showcase ![color_spaces](https://github.com/user-attachments/assets/e10f8342-c3c8-487e-b386-7acdf38d638f) --- crates/bevy_ui/src/gradients.rs | 120 +++++++++++++++++- crates/bevy_ui/src/render/gradient.rs | 26 +++- crates/bevy_ui/src/render/gradient.wgsl | 96 +++++++++++++- examples/testbed/ui.rs | 1 + examples/ui/button.rs | 4 +- examples/ui/gradients.rs | 100 +++++++++++++++ examples/ui/stacked_gradients.rs | 4 + release-content/release-notes/ui_gradients.md | 6 +- 8 files changed, 344 insertions(+), 13 deletions(-) diff --git a/crates/bevy_ui/src/gradients.rs b/crates/bevy_ui/src/gradients.rs index 969e062cd7..eb1d255cc7 100644 --- a/crates/bevy_ui/src/gradients.rs +++ b/crates/bevy_ui/src/gradients.rs @@ -3,6 +3,7 @@ use bevy_color::{Color, Srgba}; use bevy_ecs::component::Component; use bevy_math::Vec2; use bevy_reflect::prelude::*; +use bevy_utils::default; use core::{f32, f32::consts::TAU}; /// A color stop for a gradient @@ -205,7 +206,7 @@ impl Default for AngularColorStop { /// A linear gradient /// /// -#[derive(Clone, PartialEq, Debug, Reflect)] +#[derive(Default, Clone, PartialEq, Debug, Reflect)] #[reflect(PartialEq)] #[cfg_attr( feature = "serialize", @@ -213,6 +214,8 @@ impl Default for AngularColorStop { reflect(Serialize, Deserialize) )] pub struct LinearGradient { + /// The color space used for interpolation. + pub color_space: InterpolationColorSpace, /// The direction of the gradient in radians. /// An angle of `0.` points upward, with the value increasing in the clockwise direction. pub angle: f32, @@ -240,7 +243,11 @@ impl LinearGradient { /// Create a new linear gradient pub fn new(angle: f32, stops: Vec) -> Self { - Self { angle, stops } + Self { + angle, + stops, + color_space: InterpolationColorSpace::default(), + } } /// A linear gradient transitioning from bottom to top @@ -248,6 +255,7 @@ impl LinearGradient { Self { angle: Self::TO_TOP, stops, + color_space: InterpolationColorSpace::default(), } } @@ -256,6 +264,7 @@ impl LinearGradient { Self { angle: Self::TO_TOP_RIGHT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -264,6 +273,7 @@ impl LinearGradient { Self { angle: Self::TO_RIGHT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -272,6 +282,7 @@ impl LinearGradient { Self { angle: Self::TO_BOTTOM_RIGHT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -280,6 +291,7 @@ impl LinearGradient { Self { angle: Self::TO_BOTTOM, stops, + color_space: InterpolationColorSpace::default(), } } @@ -288,6 +300,7 @@ impl LinearGradient { Self { angle: Self::TO_BOTTOM_LEFT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -296,6 +309,7 @@ impl LinearGradient { Self { angle: Self::TO_LEFT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -304,6 +318,7 @@ impl LinearGradient { Self { angle: Self::TO_TOP_LEFT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -312,8 +327,14 @@ impl LinearGradient { Self { angle: degrees.to_radians(), stops, + color_space: InterpolationColorSpace::default(), } } + + pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } } /// A radial gradient @@ -327,6 +348,8 @@ impl LinearGradient { reflect(Serialize, Deserialize) )] pub struct RadialGradient { + /// The color space used for interpolation. + pub color_space: InterpolationColorSpace, /// The center of the radial gradient pub position: UiPosition, /// Defines the end shape of the radial gradient @@ -339,11 +362,17 @@ impl RadialGradient { /// Create a new radial gradient pub fn new(position: UiPosition, shape: RadialGradientShape, stops: Vec) -> Self { Self { + color_space: default(), position, shape, stops, } } + + pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } } impl Default for RadialGradient { @@ -352,6 +381,7 @@ impl Default for RadialGradient { position: UiPosition::CENTER, shape: RadialGradientShape::ClosestCorner, stops: Vec::new(), + color_space: default(), } } } @@ -359,7 +389,7 @@ impl Default for RadialGradient { /// A conic gradient /// /// -#[derive(Clone, PartialEq, Debug, Reflect)] +#[derive(Default, Clone, PartialEq, Debug, Reflect)] #[reflect(PartialEq)] #[cfg_attr( feature = "serialize", @@ -367,6 +397,8 @@ impl Default for RadialGradient { reflect(Serialize, Deserialize) )] pub struct ConicGradient { + /// The color space used for interpolation. + pub color_space: InterpolationColorSpace, /// The starting angle of the gradient in radians pub start: f32, /// The center of the conic gradient @@ -379,6 +411,7 @@ impl ConicGradient { /// Create a new conic gradient pub fn new(position: UiPosition, stops: Vec) -> Self { Self { + color_space: default(), start: 0., position, stops, @@ -396,6 +429,11 @@ impl ConicGradient { self.position = position; self } + + pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } } #[derive(Clone, PartialEq, Debug, Reflect)] @@ -573,3 +611,79 @@ impl RadialGradientShape { } } } + +/// The color space used for interpolation. +#[derive(Default, Copy, Clone, Hash, Debug, PartialEq, Eq, Reflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub enum InterpolationColorSpace { + /// Interpolates in `OKLab` space. + #[default] + OkLab, + /// Interpolates in OKLCH space, taking the shortest hue path. + OkLch, + /// Interpolates in OKLCH space, taking the longest hue path. + OkLchLong, + /// Interpolates in sRGB space. + Srgb, + /// Interpolates in linear sRGB space. + LinearRgb, +} + +/// Set the color space used for interpolation. +pub trait InColorSpace: Sized { + /// Interpolate in the given `color_space`. + fn in_color_space(self, color_space: InterpolationColorSpace) -> Self; + + /// Interpolate in `OKLab` space. + fn in_oklab(self) -> Self { + self.in_color_space(InterpolationColorSpace::OkLab) + } + + /// Interpolate in OKLCH space (short hue path). + fn in_oklch(self) -> Self { + self.in_color_space(InterpolationColorSpace::OkLch) + } + + /// Interpolate in OKLCH space (long hue path). + fn in_oklch_long(self) -> Self { + self.in_color_space(InterpolationColorSpace::OkLchLong) + } + + /// Interpolate in sRGB space. + fn in_srgb(self) -> Self { + self.in_color_space(InterpolationColorSpace::Srgb) + } + + /// Interpolate in linear sRGB space. + fn in_linear_rgb(self) -> Self { + self.in_color_space(InterpolationColorSpace::LinearRgb) + } +} + +impl InColorSpace for LinearGradient { + /// Interpolate in the given `color_space`. + fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } +} + +impl InColorSpace for RadialGradient { + /// Interpolate in the given `color_space`. + fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } +} + +impl InColorSpace for ConicGradient { + /// Interpolate in the given `color_space`. + fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } +} diff --git a/crates/bevy_ui/src/render/gradient.rs b/crates/bevy_ui/src/render/gradient.rs index bd818c7d5b..e1c845d481 100644 --- a/crates/bevy_ui/src/render/gradient.rs +++ b/crates/bevy_ui/src/render/gradient.rs @@ -140,6 +140,7 @@ pub fn compute_gradient_line_length(angle: f32, size: Vec2) -> f32 { #[derive(Clone, Copy, Hash, PartialEq, Eq)] pub struct UiGradientPipelineKey { anti_alias: bool, + color_space: InterpolationColorSpace, pub hdr: bool, } @@ -180,10 +181,18 @@ impl SpecializedRenderPipeline for GradientPipeline { VertexFormat::Float32, ], ); + let color_space = match key.color_space { + InterpolationColorSpace::OkLab => "IN_OKLAB", + InterpolationColorSpace::OkLch => "IN_OKLCH", + InterpolationColorSpace::OkLchLong => "IN_OKLCH_LONG", + InterpolationColorSpace::Srgb => "IN_SRGB", + InterpolationColorSpace::LinearRgb => "IN_LINEAR_RGB", + }; + let shader_defs = if key.anti_alias { - vec!["ANTI_ALIAS".into()] + vec![color_space.into(), "ANTI_ALIAS".into()] } else { - Vec::new() + vec![color_space.into()] }; RenderPipelineDescriptor { @@ -254,6 +263,7 @@ pub struct ExtractedGradient { /// Ordering: left, top, right, bottom. pub border: BorderRect, pub resolved_gradient: ResolvedGradient, + pub color_space: InterpolationColorSpace, } #[derive(Resource, Default)] @@ -422,7 +432,11 @@ pub fn extract_gradients( continue; } match gradient { - Gradient::Linear(LinearGradient { angle, stops }) => { + Gradient::Linear(LinearGradient { + color_space, + angle, + stops, + }) => { let length = compute_gradient_line_length(*angle, uinode.size); let range_start = extracted_color_stops.0.len(); @@ -452,9 +466,11 @@ pub fn extract_gradients( border_radius: uinode.border_radius, border: uinode.border, resolved_gradient: ResolvedGradient::Linear { angle: *angle }, + color_space: *color_space, }); } Gradient::Radial(RadialGradient { + color_space, position: center, shape, stops, @@ -500,9 +516,11 @@ pub fn extract_gradients( border_radius: uinode.border_radius, border: uinode.border, resolved_gradient: ResolvedGradient::Radial { center: c, size }, + color_space: *color_space, }); } Gradient::Conic(ConicGradient { + color_space, start, position: center, stops, @@ -557,6 +575,7 @@ pub fn extract_gradients( start: *start, center: g_start, }, + color_space: *color_space, }); } } @@ -601,6 +620,7 @@ pub fn queue_gradient( &gradients_pipeline, UiGradientPipelineKey { anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)), + color_space: gradient.color_space, hdr: view.hdr, }, ); diff --git a/crates/bevy_ui/src/render/gradient.wgsl b/crates/bevy_ui/src/render/gradient.wgsl index 0223836f2d..074cf35a35 100644 --- a/crates/bevy_ui/src/render/gradient.wgsl +++ b/crates/bevy_ui/src/render/gradient.wgsl @@ -122,6 +122,89 @@ fn mix_linear_rgb_in_srgb_space(a: vec4, b: vec4, t: f32) -> vec4 return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t)); } +fn linear_rgb_to_oklab(c: vec4) -> vec4 { + let l = pow(0.41222146 * c.x + 0.53633255 * c.y + 0.051445995 * c.z, 1. / 3.); + let m = pow(0.2119035 * c.x + 0.6806995 * c.y + 0.10739696 * c.z, 1. / 3.); + let s = pow(0.08830246 * c.x + 0.28171885 * c.y + 0.6299787 * c.z, 1. / 3.); + return vec4( + 0.21045426 * l + 0.7936178 * m - 0.004072047 * s, + 1.9779985 * l - 2.4285922 * m + 0.4505937 * s, + 0.025904037 * l + 0.78277177 * m - 0.80867577 * s, + c.w + ); +} + +fn oklab_to_linear_rgba(c: vec4) -> vec4 { + let l_ = c.x + 0.39633778 * c.y + 0.21580376 * c.z; + let m_ = c.x - 0.105561346 * c.y - 0.06385417 * c.z; + let s_ = c.x - 0.08948418 * c.y - 1.2914855 * c.z; + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + return vec4( + 4.0767417 * l - 3.3077116 * m + 0.23096994 * s, + -1.268438 * l + 2.6097574 * m - 0.34131938 * s, + -0.0041960863 * l - 0.7034186 * m + 1.7076147 * s, + c.w + ); +} + +fn mix_linear_rgb_in_oklab_space(a: vec4, b: vec4, t: f32) -> vec4 { + return oklab_to_linear_rgba(mix(linear_rgb_to_oklab(a), linear_rgb_to_oklab(b), t)); +} + +/// hue is left in radians and not converted to degrees +fn linear_rgb_to_oklch(c: vec4) -> vec4 { + let o = linear_rgb_to_oklab(c); + let chroma = sqrt(o.y * o.y + o.z * o.z); + let hue = atan2(o.z, o.y); + return vec4(o.x, chroma, select(hue + TAU, hue, hue < 0.0), o.w); +} + +fn oklch_to_linear_rgb(c: vec4) -> vec4 { + let a = c.y * cos(c.z); + let b = c.y * sin(c.z); + return oklab_to_linear_rgba(vec4(c.x, a, b, c.w)); +} + +fn rem_euclid(a: f32, b: f32) -> f32 { + return ((a % b) + b) % b; +} + +fn lerp_hue(a: f32, b: f32, t: f32) -> f32 { + let diff = rem_euclid(b - a + PI, TAU) - PI; + return rem_euclid(a + diff * t, TAU); +} + +fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 { + let diff = rem_euclid(b - a + PI, TAU) - PI; + return rem_euclid(a + select(diff - TAU, diff + TAU, 0. < diff) * t, TAU); +} + +fn mix_oklch(a: vec4, b: vec4, t: f32) -> vec4 { + return vec4( + mix(a.xy, b.xy, t), + lerp_hue(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + +fn mix_oklch_long(a: vec4, b: vec4, t: f32) -> vec4 { + return vec4( + mix(a.xy, b.xy, t), + lerp_hue_long(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + +fn mix_linear_rgb_in_oklch_space(a: vec4, b: vec4, t: f32) -> vec4 { + return oklch_to_linear_rgb(mix_oklch(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t)); +} + +fn mix_linear_rgb_in_oklch_space_long(a: vec4, b: vec4, t: f32) -> vec4 { + return oklch_to_linear_rgb(mix_oklch_long(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t)); +} + // These functions are used to calculate the distance in gradient space from the start of the gradient to the point. // The distance in gradient space is then used to interpolate between the start and end colors. @@ -192,7 +275,16 @@ fn interpolate_gradient( } else { t = 0.5 * (1 + (t - hint) / (1.0 - hint)); } - - // Only color interpolation in SRGB space is supported atm. + +#ifdef IN_SRGB return mix_linear_rgb_in_srgb_space(start_color, end_color, t); +#else ifdef IN_OKLAB + return mix_linear_rgb_in_oklab_space(start_color, end_color, t); +#else ifdef IN_OKLCH + return mix_linear_rgb_in_oklch_space(start_color, end_color, t); +#else ifdef IN_OKLCH_LONG + return mix_linear_rgb_in_oklch_space_long(start_color, end_color, t); +#else + return mix(start_color, end_color, t); +#endif } diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 6538840575..10e4e8dc8f 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -618,6 +618,7 @@ mod radial_gradient { stops: color_stops.clone(), position, shape, + ..default() }), )); }); diff --git a/examples/ui/button.rs b/examples/ui/button.rs index e533a84867..a402b5e7da 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -93,9 +93,9 @@ fn button(asset_server: &AssetServer) -> impl Bundle + use<> { align_items: AlignItems::Center, ..default() }, - BorderColor::all(Color::BLACK), + BorderColor::all(Color::WHITE), BorderRadius::MAX, - BackgroundColor(NORMAL_BUTTON), + BackgroundColor(Color::BLACK), children![( Text::new("Button"), TextFont { diff --git a/examples/ui/gradients.rs b/examples/ui/gradients.rs index a354903708..0adc930a34 100644 --- a/examples/ui/gradients.rs +++ b/examples/ui/gradients.rs @@ -12,6 +12,9 @@ use bevy::prelude::*; use bevy::ui::ColorStop; use std::f32::consts::TAU; +#[derive(Component)] +struct CurrentColorSpaceLabel; + fn main() { App::new() .add_plugins(DefaultPlugins) @@ -87,6 +90,7 @@ fn setup(mut commands: Commands) { BackgroundGradient::from(LinearGradient { angle, stops: stops.clone(), + ..default() }), BorderGradient::from(LinearGradient { angle: 3. * TAU / 8., @@ -95,6 +99,7 @@ fn setup(mut commands: Commands) { Color::WHITE.into(), ORANGE.into(), ], + ..default() }), )); } @@ -115,10 +120,12 @@ fn setup(mut commands: Commands) { BackgroundGradient::from(LinearGradient { angle: 0., stops: stops.clone(), + ..default() }), BorderGradient::from(LinearGradient { angle: 3. * TAU / 8., stops: vec![YELLOW.into(), Color::WHITE.into(), ORANGE.into()], + ..default() }), AnimateMarker, )); @@ -136,10 +143,12 @@ fn setup(mut commands: Commands) { stops: stops.clone(), shape: RadialGradientShape::ClosestSide, position: UiPosition::CENTER, + ..default() }), BorderGradient::from(LinearGradient { angle: 3. * TAU / 8., stops: vec![YELLOW.into(), Color::WHITE.into(), ORANGE.into()], + ..default() }), AnimateMarker, )); @@ -159,16 +168,107 @@ fn setup(mut commands: Commands) { .map(|stop| AngularColorStop::auto(stop.color)) .collect(), position: UiPosition::CENTER, + ..default() }), BorderGradient::from(LinearGradient { angle: 3. * TAU / 8., stops: vec![YELLOW.into(), Color::WHITE.into(), ORANGE.into()], + ..default() }), AnimateMarker, )); }); }); } + + let button = commands.spawn(( + Button, + Node { + border: UiRect::all(Val::Px(2.0)), + padding: UiRect::axes(Val::Px(8.0), Val::Px(4.0)), + // horizontally center child text + justify_content: JustifyContent::Center, + // vertically center child text + align_items: AlignItems::Center, + ..default() + }, + BorderColor::all(Color::WHITE), + BorderRadius::MAX, + BackgroundColor(Color::BLACK), + children![( + Text::new("next color space"), + TextColor(Color::srgb(0.9, 0.9, 0.9)), + TextShadow::default(), + )] + )).observe( + |_trigger: On>, mut border_query: Query<&mut BorderColor, With

; type Item<'w, 's> = Extract<'w, 's, P>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { let mut main_world = world.resource_mut::(); ExtractState { state: SystemState::new(&mut main_world), - main_world_state: Res::::init_state(world, system_meta), + main_world_state: Res::::init_state(world), } } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + Res::::init_access( + &state.main_world_state, + system_meta, + component_access_set, + world, + ); + } + #[inline] unsafe fn validate_param( state: &mut Self::State, diff --git a/release-content/migration-guides/delete_component_access.md b/release-content/migration-guides/delete_component_access.md deleted file mode 100644 index 5369c506c5..0000000000 --- a/release-content/migration-guides/delete_component_access.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: `System::component_access` has been deleted. -pull_requests: [19496] ---- - -`System::component_access` has been deleted. If you were calling this method, you can simply use -`my_system.component_access_set().combined_access()` to get the same result. - -If you were manually implementing this, it should be equivalent to `System::component_access_set` -anyway. diff --git a/release-content/migration-guides/stop_storing_system_access.md b/release-content/migration-guides/stop_storing_system_access.md new file mode 100644 index 0000000000..bb33cca03c --- /dev/null +++ b/release-content/migration-guides/stop_storing_system_access.md @@ -0,0 +1,46 @@ +--- +title: Stop storing access in systems +pull_requests: [19496, 19477] +--- + +Bevy used to store component access in all systems, +even though it was only used for top-level systems in schedules. +To reduce memory usage, the component access is now stored in the schedule instead. + +The trait methods `System::component_access` and `System::component_access_set` have been removed. +Instead, the access is returned from `System::initialize`. +If you were implementing `System` manually, the `initialize` method should return the access instead of storing it. +If you were calling `component_access` or `component_access_set` on a system that you initialized yourself, +you will need to store the access yourself. + +```rust +let system = IntoSystem::into_system(your_system); +// 0.16 +system.initialize(&mut world); +let access = system.component_access(); +// 0.17 +let component_access_set = system.initialize(&mut world); +let access = component_access_set.combined_access(); +``` + +`SystemMeta` no longer stores `FilteredAccessSet`. +It is instead passed as a separate parameter when initializing a `SystemParam`. + +To better share logic between `SystemParam` and `SystemParamBuilder`, +`SystemParam::init_state` has been split into `init_state`, which creates the state value, and `init_access`, which calculates the access. +`SystemParamBuilder::build` now only creates the state, and `SystemParam::init_access` will be called to calculate the access for built parameters. + +If you were implementing `SystemParam` manually, you will need to separate the logic into two methods +and change any uses of `system_meta.component_access_set(_mut)` to the new `component_access_set` parameter. +Note that `init_state` no longer has access to `SystemMeta` or `component_access_set`, and `init_access` only has `&state`, so the state can no longer depend on the system. + +If you were calling `init_state` manually, you will need to call `init_access` afterwards. + +```rust +// 0.16 +let param_state = P::init_state(world, &mut meta); +// 0.17 +let param_state = P::init_state(world); +let mut component_access_set = FilteredAccessSet::new(); +P::init_access(¶m_state, &mut meta, &mut component_access_set, world); +``` From a292ac539ec4ef91207985f6cc1f6becb82d0fe9 Mon Sep 17 00:00:00 2001 From: urben1680 <55257931+urben1680@users.noreply.github.com> Date: Fri, 13 Jun 2025 21:24:27 +0200 Subject: [PATCH 021/106] `System::check_change_tick` and similar methods take `CheckChangeTicks` (#19600) Follow-up of #19274. Make the `check_change_tick` methods, of which some are now public, take `CheckChangeTicks` to make it obvious where this tick comes from, see other PR. This also affects the `System` trait, hence the many changed files. --------- Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com> --- crates/bevy_ecs/src/component.rs | 14 ++++----- crates/bevy_ecs/src/entity/mod.rs | 6 ++-- crates/bevy_ecs/src/schedule/executor/mod.rs | 4 +-- crates/bevy_ecs/src/schedule/schedule.rs | 15 +++++----- crates/bevy_ecs/src/storage/resource.rs | 12 ++++---- crates/bevy_ecs/src/storage/sparse_set.rs | 10 +++---- crates/bevy_ecs/src/storage/table/column.rs | 12 ++++---- crates/bevy_ecs/src/storage/table/mod.rs | 10 +++---- crates/bevy_ecs/src/system/adapter_system.rs | 4 +-- crates/bevy_ecs/src/system/combinator.rs | 14 ++++----- .../src/system/exclusive_function_system.rs | 6 ++-- crates/bevy_ecs/src/system/function_system.rs | 6 ++-- crates/bevy_ecs/src/system/observer_system.rs | 6 ++-- crates/bevy_ecs/src/system/schedule_system.rs | 14 ++++----- crates/bevy_ecs/src/system/system.rs | 14 +++++---- crates/bevy_ecs/src/world/mod.rs | 29 ++++++++++++------- .../migration-guides/check_change_ticks.md | 25 ++++++++++++++++ 17 files changed, 119 insertions(+), 82 deletions(-) create mode 100644 release-content/migration-guides/check_change_ticks.md diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 4ebf1decd9..27b51ede6f 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -2381,12 +2381,12 @@ impl Tick { /// /// Returns `true` if wrapping was performed. Otherwise, returns `false`. #[inline] - pub fn check_tick(&mut self, tick: Tick) -> bool { - let age = tick.relative_to(*self); + pub fn check_tick(&mut self, check: CheckChangeTicks) -> bool { + let age = check.present_tick().relative_to(*self); // This comparison assumes that `age` has not overflowed `u32::MAX` before, which will be true // so long as this check always runs before that can happen. if age.get() > Self::MAX.get() { - *self = tick.relative_to(Self::MAX); + *self = check.present_tick().relative_to(Self::MAX); true } else { false @@ -2415,16 +2415,16 @@ impl Tick { /// struct CustomSchedule(Schedule); /// /// # let mut world = World::new(); -/// world.add_observer(|tick: On, mut schedule: ResMut| { -/// schedule.0.check_change_ticks(tick.get()); +/// world.add_observer(|check: On, mut schedule: ResMut| { +/// schedule.0.check_change_ticks(*check); /// }); /// ``` #[derive(Debug, Clone, Copy, Event)] pub struct CheckChangeTicks(pub(crate) Tick); impl CheckChangeTicks { - /// Get the `Tick` that can be used as the parameter of [`Tick::check_tick`]. - pub fn get(self) -> Tick { + /// Get the present `Tick` that other ticks get compared to. + pub fn present_tick(self) -> Tick { self.0 } } diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 0e22fddbc8..02d9698917 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -76,7 +76,7 @@ pub use unique_vec::{UniqueEntityEquivalentVec, UniqueEntityVec}; use crate::{ archetype::{ArchetypeId, ArchetypeRow}, change_detection::MaybeLocation, - component::Tick, + component::{CheckChangeTicks, Tick}, storage::{SparseSetIndex, TableId, TableRow}, }; use alloc::vec::Vec; @@ -1216,9 +1216,9 @@ impl Entities { } #[inline] - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for meta in &mut self.meta { - meta.spawned_or_despawned.at.check_tick(change_tick); + meta.spawned_or_despawned.at.check_tick(check); } } diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 710b88db6c..c1fac6a9f1 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -15,7 +15,7 @@ pub use self::multi_threaded::{MainThreadExecutor, MultiThreadedExecutor}; use fixedbitset::FixedBitSet; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::{BevyError, ErrorContext, Result}, prelude::{IntoSystemSet, SystemSet}, query::FilteredAccessSet, @@ -204,7 +204,7 @@ impl System for ApplyDeferred { FilteredAccessSet::new() } - fn check_change_tick(&mut self, _change_tick: Tick) {} + fn check_change_tick(&mut self, _check: CheckChangeTicks) {} fn default_system_sets(&self) -> Vec { vec![SystemTypeSet::::new().intern()] diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 73c710a455..0e4f530be0 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -25,8 +25,9 @@ use thiserror::Error; #[cfg(feature = "trace")] use tracing::info_span; +use crate::component::CheckChangeTicks; use crate::{ - component::{ComponentId, Components, Tick}, + component::{ComponentId, Components}, prelude::Component, query::FilteredAccessSet, resource::Resource, @@ -112,7 +113,7 @@ impl Schedules { /// Iterates the change ticks of all systems in all stored schedules and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { #[cfg(feature = "trace")] let _all_span = info_span!("check stored schedule ticks").entered(); #[cfg_attr( @@ -127,7 +128,7 @@ impl Schedules { let name = format!("{label:?}"); #[cfg(feature = "trace")] let _one_span = info_span!("check schedule ticks", name = &name).entered(); - schedule.check_change_ticks(change_tick); + schedule.check_change_ticks(check); } } @@ -559,22 +560,22 @@ impl Schedule { /// Iterates the change ticks of all systems in the schedule and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. - pub fn check_change_ticks(&mut self, change_tick: Tick) { + pub fn check_change_ticks(&mut self, check: CheckChangeTicks) { for SystemWithAccess { system, .. } in &mut self.executable.systems { if !is_apply_deferred(system) { - system.check_change_tick(change_tick); + system.check_change_tick(check); } } for conditions in &mut self.executable.system_conditions { for system in conditions { - system.condition.check_change_tick(change_tick); + system.condition.check_change_tick(check); } } for conditions in &mut self.executable.set_conditions { for system in conditions { - system.condition.check_change_tick(change_tick); + system.condition.check_change_tick(check); } } } diff --git a/crates/bevy_ecs/src/storage/resource.rs b/crates/bevy_ecs/src/storage/resource.rs index fa58610bdf..fc9100069c 100644 --- a/crates/bevy_ecs/src/storage/resource.rs +++ b/crates/bevy_ecs/src/storage/resource.rs @@ -1,6 +1,6 @@ use crate::{ change_detection::{MaybeLocation, MutUntyped, TicksMut}, - component::{ComponentId, ComponentTicks, Components, Tick, TickCells}, + component::{CheckChangeTicks, ComponentId, ComponentTicks, Components, Tick, TickCells}, storage::{blob_vec::BlobVec, SparseSet}, }; use alloc::string::String; @@ -298,9 +298,9 @@ impl ResourceData { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - self.added_ticks.get_mut().check_tick(change_tick); - self.changed_ticks.get_mut().check_tick(change_tick); + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { + self.added_ticks.get_mut().check_tick(check); + self.changed_ticks.get_mut().check_tick(check); } } @@ -393,9 +393,9 @@ impl Resources { }) } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for info in self.resources.values_mut() { - info.check_change_ticks(change_tick); + info.check_change_ticks(check); } } } diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index 42adcd89dc..bb28f967af 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -1,6 +1,6 @@ use crate::{ change_detection::MaybeLocation, - component::{ComponentId, ComponentInfo, ComponentTicks, Tick, TickCells}, + component::{CheckChangeTicks, ComponentId, ComponentInfo, ComponentTicks, Tick, TickCells}, entity::{Entity, EntityRow}, storage::{Column, TableRow}, }; @@ -360,8 +360,8 @@ impl ComponentSparseSet { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - self.dense.check_change_ticks(change_tick); + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { + self.dense.check_change_ticks(check); } } @@ -650,9 +650,9 @@ impl SparseSets { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for set in self.sets.values_mut() { - set.check_change_ticks(change_tick); + set.check_change_ticks(check); } } } diff --git a/crates/bevy_ecs/src/storage/table/column.rs b/crates/bevy_ecs/src/storage/table/column.rs index 78fafe0a26..acf531d9b9 100644 --- a/crates/bevy_ecs/src/storage/table/column.rs +++ b/crates/bevy_ecs/src/storage/table/column.rs @@ -228,20 +228,20 @@ impl ThinColumn { /// # Safety /// `len` is the actual length of this column #[inline] - pub(crate) unsafe fn check_change_ticks(&mut self, len: usize, change_tick: Tick) { + pub(crate) unsafe fn check_change_ticks(&mut self, len: usize, check: CheckChangeTicks) { for i in 0..len { // SAFETY: // - `i` < `len` // we have a mutable reference to `self` unsafe { self.added_ticks.get_unchecked_mut(i) } .get_mut() - .check_tick(change_tick); + .check_tick(check); // SAFETY: // - `i` < `len` // we have a mutable reference to `self` unsafe { self.changed_ticks.get_unchecked_mut(i) } .get_mut() - .check_tick(change_tick); + .check_tick(check); } } @@ -646,12 +646,12 @@ impl Column { } #[inline] - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for component_ticks in &mut self.added_ticks { - component_ticks.get_mut().check_tick(change_tick); + component_ticks.get_mut().check_tick(check); } for component_ticks in &mut self.changed_ticks { - component_ticks.get_mut().check_tick(change_tick); + component_ticks.get_mut().check_tick(check); } } diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index 5f09d4226f..be75c58f03 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -1,6 +1,6 @@ use crate::{ change_detection::MaybeLocation, - component::{ComponentId, ComponentInfo, ComponentTicks, Components, Tick}, + component::{CheckChangeTicks, ComponentId, ComponentInfo, ComponentTicks, Components, Tick}, entity::Entity, query::DebugCheckedUnwrap, storage::{blob_vec::BlobVec, ImmutableSparseSet, SparseSet}, @@ -629,11 +629,11 @@ impl Table { } /// Call [`Tick::check_tick`] on all of the ticks in the [`Table`] - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { let len = self.entity_count() as usize; for col in self.columns.values_mut() { // SAFETY: `len` is the actual length of the column - unsafe { col.check_change_ticks(len, change_tick) }; + unsafe { col.check_change_ticks(len, check) }; } } @@ -793,9 +793,9 @@ impl Tables { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for table in &mut self.tables { - table.check_change_ticks(change_tick); + table.check_change_ticks(check); } } } diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index 6573f851b9..ff63a3e8ce 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -176,8 +176,8 @@ where self.system.initialize(world) } - fn check_change_tick(&mut self, change_tick: crate::component::Tick) { - self.system.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: crate::component::CheckChangeTicks) { + self.system.check_change_tick(check); } fn default_system_sets(&self) -> Vec { diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index f892507ed6..976d654a59 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -2,7 +2,7 @@ use alloc::{borrow::Cow, format, vec::Vec}; use core::marker::PhantomData; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, prelude::World, query::FilteredAccessSet, schedule::InternedSystemSet, @@ -200,9 +200,9 @@ where a_access } - fn check_change_tick(&mut self, change_tick: Tick) { - self.a.check_change_tick(change_tick); - self.b.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.a.check_change_tick(check); + self.b.check_change_tick(check); } fn default_system_sets(&self) -> Vec { @@ -406,9 +406,9 @@ where a_access } - fn check_change_tick(&mut self, change_tick: Tick) { - self.a.check_change_tick(change_tick); - self.b.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.a.check_change_tick(check); + self.b.check_change_tick(check); } fn default_system_sets(&self) -> Vec { diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 32d76649b8..c6bdb002c2 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -1,5 +1,5 @@ use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, system::{ @@ -177,10 +177,10 @@ where } #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { + fn check_change_tick(&mut self, check: CheckChangeTicks) { check_system_change_tick( &mut self.system_meta.last_run, - change_tick, + check, self.system_meta.name.as_ref(), ); } diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 22ecef2104..13e531648d 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -1,5 +1,5 @@ use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, prelude::FromWorld, query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, @@ -708,10 +708,10 @@ where } #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { + fn check_change_tick(&mut self, check: CheckChangeTicks) { check_system_change_tick( &mut self.system_meta.last_run, - change_tick, + check, self.system_meta.name.as_ref(), ); } diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index 9c69b95dd3..e99a86b64c 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -2,7 +2,7 @@ use alloc::{borrow::Cow, vec::Vec}; use core::marker::PhantomData; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::Result, never::Never, prelude::{Bundle, On}, @@ -161,8 +161,8 @@ where } #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { - self.observer.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.observer.check_change_tick(check); } #[inline] diff --git a/crates/bevy_ecs/src/system/schedule_system.rs b/crates/bevy_ecs/src/system/schedule_system.rs index 35682d7f3b..ab2d9d31fb 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -1,7 +1,7 @@ use alloc::{borrow::Cow, vec::Vec}; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::Result, query::FilteredAccessSet, system::{input::SystemIn, BoxedSystem, System, SystemInput}, @@ -78,8 +78,8 @@ impl> System for InfallibleSystemWrapper { } #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { - self.0.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.0.check_change_tick(check); } #[inline] @@ -182,8 +182,8 @@ where self.system.initialize(world) } - fn check_change_tick(&mut self, change_tick: Tick) { - self.system.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.system.check_change_tick(check); } fn get_last_run(&self) -> Tick { @@ -283,8 +283,8 @@ where self.system.initialize(world) } - fn check_change_tick(&mut self, change_tick: Tick) { - self.system.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.system.check_change_tick(check); } fn get_last_run(&self) -> Tick { diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index d23f54e8f4..3b4c99858f 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -8,7 +8,7 @@ use log::warn; use thiserror::Error; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, query::FilteredAccessSet, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn}, @@ -174,7 +174,7 @@ pub trait System: Send + Sync + 'static { /// /// This method must be called periodically to ensure that change detection behaves correctly. /// When using bevy's default configuration, this will be called for you as needed. - fn check_change_tick(&mut self, change_tick: Tick); + fn check_change_tick(&mut self, check: CheckChangeTicks); /// Returns the system's default [system sets](crate::schedule::SystemSet). /// @@ -224,9 +224,13 @@ pub unsafe trait ReadOnlySystem: System { /// A convenience type alias for a boxed [`System`] trait object. pub type BoxedSystem = Box>; -pub(crate) fn check_system_change_tick(last_run: &mut Tick, this_run: Tick, system_name: &str) { - if last_run.check_tick(this_run) { - let age = this_run.relative_to(*last_run).get(); +pub(crate) fn check_system_change_tick( + last_run: &mut Tick, + check: CheckChangeTicks, + system_name: &str, +) { + if last_run.check_tick(check) { + let age = check.present_tick().relative_to(*last_run).get(); warn!( "System '{system_name}' has not run for {age} ticks. \ Changes older than {} ticks will not be detected.", diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 1774da9728..dbc537fc8e 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -2937,17 +2937,21 @@ impl World { } /// Iterates all component change ticks and clamps any older than [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). - /// This prevents overflow and thus prevents false positives. + /// This also triggers [`CheckChangeTicks`] observers and returns the same event here. /// - /// **Note:** Does nothing if the [`World`] counter has not been incremented at least [`CHECK_TICK_THRESHOLD`] + /// Calling this method prevents [`Tick`]s overflowing and thus prevents false positives when comparing them. + /// + /// **Note:** Does nothing and returns `None` if the [`World`] counter has not been incremented at least [`CHECK_TICK_THRESHOLD`] /// times since the previous pass. // TODO: benchmark and optimize - pub fn check_change_ticks(&mut self) { + pub fn check_change_ticks(&mut self) -> Option { let change_tick = self.change_tick(); if change_tick.relative_to(self.last_check_tick).get() < CHECK_TICK_THRESHOLD { - return; + return None; } + let check = CheckChangeTicks(change_tick); + let Storages { ref mut tables, ref mut sparse_sets, @@ -2957,19 +2961,22 @@ impl World { #[cfg(feature = "trace")] let _span = tracing::info_span!("check component ticks").entered(); - tables.check_change_ticks(change_tick); - sparse_sets.check_change_ticks(change_tick); - resources.check_change_ticks(change_tick); - non_send_resources.check_change_ticks(change_tick); - self.entities.check_change_ticks(change_tick); + tables.check_change_ticks(check); + sparse_sets.check_change_ticks(check); + resources.check_change_ticks(check); + non_send_resources.check_change_ticks(check); + self.entities.check_change_ticks(check); if let Some(mut schedules) = self.get_resource_mut::() { - schedules.check_change_ticks(change_tick); + schedules.check_change_ticks(check); } - self.trigger(CheckChangeTicks(change_tick)); + self.trigger(check); + self.flush(); self.last_check_tick = change_tick; + + Some(check) } /// Runs both [`clear_entities`](Self::clear_entities) and [`clear_resources`](Self::clear_resources), diff --git a/release-content/migration-guides/check_change_ticks.md b/release-content/migration-guides/check_change_ticks.md new file mode 100644 index 0000000000..109be4b12a --- /dev/null +++ b/release-content/migration-guides/check_change_ticks.md @@ -0,0 +1,25 @@ +--- +title: `CheckChangeTicks` parameter in `System::check_change_tick` +pull_requests: [19274, 19600] +--- + +`System::check_change_tick` took a `Tick` parameter to update internal ticks. This is needed to keep queried components filtered by their change tick reliably not be matched if their last change or add and the system's last run was very long ago. This is also needed for similar methods involving the system's ticks for the same reason. + +This parameter is now a `CheckChangeTicks` type that can be passed to the now-public `Tick::check_tick` in case you maintain these yourself in manual `System` implementations. + +If you need a `CheckChangeTicks` value, for example because you call one of the above methods manually, you can observe it. Here is an example where it is used on a schedule stored in a resource, which will pass it on to the `System::check_change_tick` of its systems. + +```rs +use bevy_ecs::prelude::*; +use bevy_ecs::component::CheckChangeTicks; + +#[derive(Resource)] +struct CustomSchedule(Schedule); + +let mut world = World::new(); +world.add_observer(|check: On, mut schedule: ResMut| { + schedule.0.check_change_ticks(*check); +}); +``` + +The observers are triggered by `World::check_change_ticks` which every schedule calls before running. This method also returns an `Option` which is `Some` in case it was time to check the ticks. From 2b0a05cbb85a75198f636031ef6053c03d8aeba6 Mon Sep 17 00:00:00 2001 From: Design_Dream <32071499+chendaohan@users.noreply.github.com> Date: Sat, 14 Jun 2025 03:31:09 +0800 Subject: [PATCH 022/106] Fix incorrect description of ClusteredDecal (#19630) The documentation states that ClusteredDecal projects in the +Z direction, but in practice, it projects in the -Z direction, which can be confusing. # Objective Fixes #19612 --- crates/bevy_pbr/src/decal/clustered.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index dd77d2088d..5618b31831 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -69,7 +69,7 @@ pub struct ClusteredDecalPlugin; /// An object that projects a decal onto surfaces within its bounds. /// /// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It -/// projects the given [`Self::image`] onto surfaces in the +Z direction (thus +/// projects the given [`Self::image`] onto surfaces in the -Z direction (thus /// you may find [`Transform::looking_at`] useful). /// /// Clustered decals are the highest-quality types of decals that Bevy supports, From f47b1c00ee6c55f98f1858db6d6bc1fc1a4bed0e Mon Sep 17 00:00:00 2001 From: andriyDev Date: Fri, 13 Jun 2025 12:54:31 -0700 Subject: [PATCH 023/106] Bump ron version to 0.10. (#19631) # Objective - Update ron to the latest version. - This is blocking changes to AnimationGraph (as some valid structs are not capable of being deserialized). ## Solution - Bump ron! ## Testing - The particular issue I was blocked by seems to be resolved! --- Cargo.toml | 2 +- crates/bevy_animation/Cargo.toml | 2 +- crates/bevy_animation/src/graph.rs | 3 ++- crates/bevy_asset/Cargo.toml | 2 +- crates/bevy_dev_tools/Cargo.toml | 2 +- crates/bevy_reflect/Cargo.toml | 2 +- crates/bevy_reflect/src/serde/ser/processor.rs | 8 ++++---- examples/animation/animation_graph.rs | 14 ++++++++------ tools/example-showcase/Cargo.toml | 2 +- 9 files changed, 20 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1dd5410428..ed2b369f5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -562,7 +562,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" 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_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_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_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 8827fc695b..4d72ab1d6a 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -114,7 +114,7 @@ wgpu-types = { version = "24", features = [ ], optional = true, default-features = false } [dev-dependencies] -ron = "0.8.0" +ron = "0.10" rmp-serde = "1.1" bincode = { version = "2.0", features = ["serde"] } serde_json = "1.0.140" diff --git a/crates/bevy_reflect/src/serde/ser/processor.rs b/crates/bevy_reflect/src/serde/ser/processor.rs index cf31ab7566..fc35ff883a 100644 --- a/crates/bevy_reflect/src/serde/ser/processor.rs +++ b/crates/bevy_reflect/src/serde/ser/processor.rs @@ -112,15 +112,15 @@ use crate::{PartialReflect, TypeRegistry}; /// } /// } /// -/// fn save(type_registry: &TypeRegistry, asset: &MyAsset) -> Result, AssetError> { -/// let mut asset_bytes = Vec::new(); +/// fn save(type_registry: &TypeRegistry, asset: &MyAsset) -> Result { +/// let mut asset_string = String::new(); /// /// let processor = HandleProcessor; /// let serializer = ReflectSerializer::with_processor(asset, type_registry, &processor); -/// let mut ron_serializer = ron::Serializer::new(&mut asset_bytes, None)?; +/// let mut ron_serializer = ron::Serializer::new(&mut asset_string, None)?; /// /// serializer.serialize(&mut ron_serializer)?; -/// Ok(asset_bytes) +/// Ok(asset_string) /// } /// ``` /// diff --git a/examples/animation/animation_graph.rs b/examples/animation/animation_graph.rs index 884ec1a2af..f420b05f38 100644 --- a/examples/animation/animation_graph.rs +++ b/examples/animation/animation_graph.rs @@ -180,17 +180,19 @@ fn setup_assets_programmatically( IoTaskPool::get() .spawn(async move { + use std::io::Write; + + let serialized_graph = + ron::ser::to_string_pretty(&animation_graph, PrettyConfig::default()) + .expect("Failed to serialize the animation graph"); let mut animation_graph_writer = File::create(Path::join( &FileAssetReader::get_base_path(), Path::join(Path::new("assets"), Path::new(ANIMATION_GRAPH_PATH)), )) .expect("Failed to open the animation graph asset"); - ron::ser::to_writer_pretty( - &mut animation_graph_writer, - &animation_graph, - PrettyConfig::default(), - ) - .expect("Failed to serialize the animation graph"); + animation_graph_writer + .write_all(serialized_graph.as_bytes()) + .expect("Failed to write the animation graph"); }) .detach(); } diff --git a/tools/example-showcase/Cargo.toml b/tools/example-showcase/Cargo.toml index f97dab44a7..2a54af4a68 100644 --- a/tools/example-showcase/Cargo.toml +++ b/tools/example-showcase/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [dependencies] xshell = "0.2" clap = { version = "4.0", features = ["derive"] } -ron = "0.8" +ron = "0.10" toml_edit = { version = "0.22.7", default-features = false, features = [ "parse", ] } From 30aa36eaf47ba7b8518e80e560fec1a7c3d39a14 Mon Sep 17 00:00:00 2001 From: Talin Date: Sat, 14 Jun 2025 17:53:31 -0700 Subject: [PATCH 024/106] Core slider (#19584) # Objective This is part of the "core widgets" effort: #19236. ## Solution This PR adds the "core slider" widget to the collection. ## Testing Tested using examples `core_widgets` and `core_widgets_observers`. --------- Co-authored-by: ickshonpe --- crates/bevy_core_widgets/Cargo.toml | 8 +- crates/bevy_core_widgets/src/core_slider.rs | 509 ++++++++++++++++++ crates/bevy_core_widgets/src/lib.rs | 11 +- examples/ui/core_widgets.rs | 277 ++++++++-- examples/ui/core_widgets_observers.rs | 313 +++++++++-- .../release-notes/headless-widgets.md | 12 +- 6 files changed, 1044 insertions(+), 86 deletions(-) create mode 100644 crates/bevy_core_widgets/src/core_slider.rs diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml index 1627ff9a29..83ecd67ede 100644 --- a/crates/bevy_core_widgets/Cargo.toml +++ b/crates/bevy_core_widgets/Cargo.toml @@ -15,8 +15,14 @@ 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_ui = { path = "../bevy_ui", 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" 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..b230442528 --- /dev/null +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -0,0 +1,509 @@ +use core::ops::RangeInclusive; + +use accesskit::{Orientation, Role}; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::event::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, +) { + if q_thumb.contains(trigger.target().unwrap()) { + // 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().unwrap()) + .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().unwrap()) + { + // Track click + trigger.propagate(false); + + // Set focus to slider and hide focus ring + if let Some(mut focus) = focus { + focus.0 = 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().unwrap()) + .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(), + ); + 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().unwrap()) + .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().unwrap()) { + 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().unwrap()) + { + 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().unwrap()) + .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().unwrap()) + .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().unwrap()) { + 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().unwrap()) { + 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); + } + } + } +} + +pub(crate) fn slider_on_insert(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target().unwrap()); + 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().unwrap()); + 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().unwrap()); + 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().unwrap()); + let step = entity.get::().unwrap().0; + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_numeric_value_step(step.into()); + } +} + +/// Event which can be triggered on a slider to modify the 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)] +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().unwrap()) { + 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().unwrap()) + .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 index afeed92a1f..00812bddfc 100644 --- a/crates/bevy_core_widgets/src/lib.rs +++ b/crates/bevy_core_widgets/src/lib.rs @@ -10,11 +10,20 @@ //! 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_slider; use bevy_app::{App, Plugin}; pub use core_button::{CoreButton, CoreButtonPlugin}; +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. @@ -22,6 +31,6 @@ pub struct CoreWidgetsPlugin; impl Plugin for CoreWidgetsPlugin { fn build(&self, app: &mut App) { - app.add_plugins(CoreButtonPlugin); + app.add_plugins((CoreButtonPlugin, CoreSliderPlugin)); } } diff --git a/examples/ui/core_widgets.rs b/examples/ui/core_widgets.rs index 594f43efd8..27884855fa 100644 --- a/examples/ui/core_widgets.rs +++ b/examples/ui/core_widgets.rs @@ -2,7 +2,10 @@ use bevy::{ color::palettes::basic::*, - core_widgets::{CoreButton, CoreWidgetsPlugin}, + core_widgets::{ + CoreButton, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue, + TrackClick, + }, ecs::system::SystemId, input_focus::{ tab_navigation::{TabGroup, TabIndex}, @@ -19,10 +22,18 @@ fn main() { .add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin)) // Only run the app when there is user input. This will significantly reduce CPU/GPU use. .insert_resource(WinitSettings::desktop_app()) + .insert_resource(DemoWidgetStates { slider_value: 50.0 }) .add_systems(Startup, setup) .add_systems( Update, - (update_button_style, update_button_style2, toggle_disabled), + ( + update_widget_values, + update_button_style, + update_button_style2, + update_slider_style.after(update_widget_values), + update_slider_style2.after(update_widget_values), + toggle_disabled, + ), ) .run(); } @@ -30,11 +41,32 @@ fn main() { const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); +const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05); +const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35); /// Marker which identifies buttons with a particular style, in this case the "Demo style". #[derive(Component)] struct DemoButton; +/// Marker which identifies sliders with a particular style. +#[derive(Component, Default)] +struct DemoSlider; + +/// Marker which identifies the slider's thumb element. +#[derive(Component, Default)] +struct DemoSliderThumb; + +/// A struct to hold the state of various widgets shown in the demo. +/// +/// While it is possible to use the widget's own state components as the source of truth, +/// in many cases widgets will be used to display dynamic data coming from deeper within the app, +/// using some kind of data-binding. This example shows how to maintain an external source of +/// truth for widget states. +#[derive(Resource)] +struct DemoWidgetStates { + slider_value: f32, +} + fn update_button_style( mut buttons: Query< ( @@ -157,16 +189,45 @@ fn set_button_style( } } +/// Update the widget states based on the changing resource. +fn update_widget_values( + res: Res, + mut sliders: Query>, + mut commands: Commands, +) { + if res.is_changed() { + for slider_ent in sliders.iter_mut() { + commands + .entity(slider_ent) + .insert(SliderValue(res.slider_value)); + } + } +} + fn setup(mut commands: Commands, assets: Res) { + // System to print a value when the button is clicked. let on_click = commands.register_system(|| { info!("Button clicked!"); }); + + // System to update a resource when the slider value changes. Note that we could have + // updated the slider value directly, but we want to demonstrate externalizing the state. + let on_change_value = commands.register_system( + |value: In, mut widget_states: ResMut| { + widget_states.slider_value = *value; + }, + ); + // ui camera commands.spawn(Camera2d); - commands.spawn(button(&assets, on_click)); + commands.spawn(demo_root(&assets, on_click, on_change_value)); } -fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { +fn demo_root( + asset_server: &AssetServer, + on_click: SystemId, + on_change_value: SystemId>, +) -> impl Bundle { ( Node { width: Val::Percent(100.0), @@ -175,57 +236,197 @@ fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { justify_content: JustifyContent::Center, display: Display::Flex, flex_direction: FlexDirection::Column, + row_gap: Val::Px(10.0), ..default() }, TabGroup::default(), children![ - ( - Node { - width: Val::Px(150.0), - height: Val::Px(65.0), - border: UiRect::all(Val::Px(5.0)), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }, - DemoButton, - CoreButton { - on_click: Some(on_click), - }, - Hovered::default(), - TabIndex(0), - BorderColor::all(Color::BLACK), - BorderRadius::MAX, - BackgroundColor(NORMAL_BUTTON), - children![( - Text::new("Button"), - TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 33.0, - ..default() - }, - TextColor(Color::srgb(0.9, 0.9, 0.9)), - TextShadow::default(), - )] - ), - Text::new("Press 'D' to toggle button disabled state"), + button(asset_server, on_click), + slider(0.0, 100.0, 50.0, Some(on_change_value)), + Text::new("Press 'D' to toggle widget disabled states"), ], ) } +fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { + ( + Node { + width: Val::Px(150.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + DemoButton, + CoreButton { + on_click: Some(on_click), + }, + Hovered::default(), + TabIndex(0), + BorderColor::all(Color::BLACK), + BorderRadius::MAX, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Button"), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + TextShadow::default(), + )], + ) +} + +/// Create a demo slider +fn slider(min: f32, max: f32, value: f32, on_change: Option>>) -> impl Bundle { + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Stretch, + justify_items: JustifyItems::Center, + column_gap: Val::Px(4.0), + height: Val::Px(12.0), + width: Val::Percent(30.0), + ..default() + }, + Name::new("Slider"), + Hovered::default(), + DemoSlider, + CoreSlider { + on_change, + track_click: TrackClick::Snap, + }, + SliderValue(value), + SliderRange::new(min, max), + TabIndex(0), + Children::spawn(( + // Slider background rail + Spawn(( + Node { + height: Val::Px(6.0), + ..default() + }, + BackgroundColor(SLIDER_TRACK), // Border color for the checkbox + BorderRadius::all(Val::Px(3.0)), + )), + // Invisible track to allow absolute placement of thumb entity. This is narrower than + // the actual slider, which allows us to position the thumb entity using simple + // percentages, without having to measure the actual width of the slider thumb. + Spawn(( + Node { + display: Display::Flex, + position_type: PositionType::Absolute, + left: Val::Px(0.0), + // Track is short by 12px to accommodate the thumb. + right: Val::Px(12.0), + top: Val::Px(0.0), + bottom: Val::Px(0.0), + ..default() + }, + children![( + // Thumb + DemoSliderThumb, + CoreSliderThumb, + Node { + display: Display::Flex, + width: Val::Px(12.0), + height: Val::Px(12.0), + position_type: PositionType::Absolute, + left: Val::Percent(0.0), // This will be updated by the slider's value + ..default() + }, + BorderRadius::MAX, + BackgroundColor(SLIDER_THUMB), + )], + )), + )), + ) +} + +/// Update the visuals of the slider based on the slider state. +fn update_slider_style( + sliders: Query< + ( + Entity, + &SliderValue, + &SliderRange, + &Hovered, + Has, + ), + ( + Or<( + Changed, + Changed, + Changed, + Added, + )>, + With, + ), + >, + children: Query<&Children>, + mut thumbs: Query<(&mut Node, &mut BackgroundColor, Has), Without>, +) { + for (slider_ent, value, range, hovered, disabled) in sliders.iter() { + for child in children.iter_descendants(slider_ent) { + if let Ok((mut thumb_node, mut thumb_bg, is_thumb)) = thumbs.get_mut(child) { + if is_thumb { + thumb_node.left = Val::Percent(range.thumb_position(value.0) * 100.0); + thumb_bg.0 = thumb_color(disabled, hovered.0); + } + } + } + } +} + +fn update_slider_style2( + sliders: Query<(Entity, &Hovered, Has), With>, + children: Query<&Children>, + mut thumbs: Query<(&mut BackgroundColor, Has), Without>, + mut removed_disabled: RemovedComponents, +) { + removed_disabled.read().for_each(|entity| { + if let Ok((slider_ent, hovered, disabled)) = sliders.get(entity) { + for child in children.iter_descendants(slider_ent) { + if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child) { + if is_thumb { + thumb_bg.0 = thumb_color(disabled, hovered.0); + } + } + } + } + }); +} + +fn thumb_color(disabled: bool, hovered: bool) -> Color { + match (disabled, hovered) { + (true, _) => GRAY.into(), + + (false, true) => SLIDER_THUMB.lighter(0.3), + + _ => SLIDER_THUMB, + } +} + fn toggle_disabled( input: Res>, - mut interaction_query: Query<(Entity, Has), With>, + mut interaction_query: Query< + (Entity, Has), + Or<(With, With)>, + >, mut commands: Commands, ) { if input.just_pressed(KeyCode::KeyD) { for (entity, disabled) in &mut interaction_query { - // disabled.0 = !disabled.0; if disabled { - info!("Button enabled"); + info!("Widgets enabled"); commands.entity(entity).remove::(); } else { - info!("Button disabled"); + info!("Widgets disabled"); commands.entity(entity).insert(InteractionDisabled); } } diff --git a/examples/ui/core_widgets_observers.rs b/examples/ui/core_widgets_observers.rs index bda346b491..bec0244ce0 100644 --- a/examples/ui/core_widgets_observers.rs +++ b/examples/ui/core_widgets_observers.rs @@ -2,7 +2,9 @@ use bevy::{ color::palettes::basic::*, - core_widgets::{CoreButton, CoreWidgetsPlugin}, + core_widgets::{ + CoreButton, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue, + }, ecs::system::SystemId, input_focus::{ tab_navigation::{TabGroup, TabIndex}, @@ -19,25 +21,52 @@ fn main() { .add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin)) // Only run the app when there is user input. This will significantly reduce CPU/GPU use. .insert_resource(WinitSettings::desktop_app()) + .insert_resource(DemoWidgetStates { slider_value: 50.0 }) .add_systems(Startup, setup) - .add_observer(on_add_pressed) - .add_observer(on_remove_pressed) - .add_observer(on_add_disabled) - .add_observer(on_remove_disabled) - .add_observer(on_change_hover) - .add_systems(Update, toggle_disabled) + .add_observer(button_on_add_pressed) + .add_observer(button_on_remove_pressed) + .add_observer(button_on_add_disabled) + .add_observer(button_on_remove_disabled) + .add_observer(button_on_change_hover) + .add_observer(slider_on_add_disabled) + .add_observer(slider_on_remove_disabled) + .add_observer(slider_on_change_hover) + .add_observer(slider_on_change_value) + .add_observer(slider_on_change_range) + .add_systems(Update, (update_widget_values, toggle_disabled)) .run(); } const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); +const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05); +const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35); /// Marker which identifies buttons with a particular style, in this case the "Demo style". #[derive(Component)] struct DemoButton; -fn on_add_pressed( +/// Marker which identifies sliders with a particular style. +#[derive(Component, Default)] +struct DemoSlider; + +/// Marker which identifies the slider's thumb element. +#[derive(Component, Default)] +struct DemoSliderThumb; + +/// A struct to hold the state of various widgets shown in the demo. +/// +/// While it is possible to use the widget's own state components as the source of truth, +/// in many cases widgets will be used to display dynamic data coming from deeper within the app, +/// using some kind of data-binding. This example shows how to maintain an external source of +/// truth for widget states. +#[derive(Resource)] +struct DemoWidgetStates { + slider_value: f32, +} + +fn button_on_add_pressed( trigger: On, mut buttons: Query< ( @@ -66,7 +95,7 @@ fn on_add_pressed( } } -fn on_remove_pressed( +fn button_on_remove_pressed( trigger: On, mut buttons: Query< ( @@ -95,7 +124,7 @@ fn on_remove_pressed( } } -fn on_add_disabled( +fn button_on_add_disabled( trigger: On, mut buttons: Query< ( @@ -124,7 +153,7 @@ fn on_add_disabled( } } -fn on_remove_disabled( +fn button_on_remove_disabled( trigger: On, mut buttons: Query< ( @@ -153,7 +182,7 @@ fn on_remove_disabled( } } -fn on_change_hover( +fn button_on_change_hover( trigger: On, mut buttons: Query< ( @@ -227,16 +256,140 @@ fn set_button_style( } } +fn slider_on_add_disabled( + trigger: On, + sliders: Query<(Entity, &Hovered), With>, + children: Query<&Children>, + mut thumbs: Query<(&mut BackgroundColor, Has), Without>, +) { + if let Ok((slider_ent, hovered)) = sliders.get(trigger.target().unwrap()) { + for child in children.iter_descendants(slider_ent) { + if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child) { + if is_thumb { + thumb_bg.0 = thumb_color(true, hovered.0); + } + } + } + } +} + +fn slider_on_remove_disabled( + trigger: On, + sliders: Query<(Entity, &Hovered), With>, + children: Query<&Children>, + mut thumbs: Query<(&mut BackgroundColor, Has), Without>, +) { + if let Ok((slider_ent, hovered)) = sliders.get(trigger.target().unwrap()) { + for child in children.iter_descendants(slider_ent) { + if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child) { + if is_thumb { + thumb_bg.0 = thumb_color(false, hovered.0); + } + } + } + } +} + +fn slider_on_change_hover( + trigger: On, + sliders: Query<(Entity, &Hovered, Has), With>, + children: Query<&Children>, + mut thumbs: Query<(&mut BackgroundColor, Has), Without>, +) { + if let Ok((slider_ent, hovered, disabled)) = sliders.get(trigger.target().unwrap()) { + for child in children.iter_descendants(slider_ent) { + if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child) { + if is_thumb { + thumb_bg.0 = thumb_color(disabled, hovered.0); + } + } + } + } +} + +fn slider_on_change_value( + trigger: On, + sliders: Query<(Entity, &SliderValue, &SliderRange), With>, + children: Query<&Children>, + mut thumbs: Query<(&mut Node, Has), Without>, +) { + if let Ok((slider_ent, value, range)) = sliders.get(trigger.target().unwrap()) { + for child in children.iter_descendants(slider_ent) { + if let Ok((mut thumb_node, is_thumb)) = thumbs.get_mut(child) { + if is_thumb { + thumb_node.left = Val::Percent(range.thumb_position(value.0) * 100.0); + } + } + } + } +} + +fn slider_on_change_range( + trigger: On, + sliders: Query<(Entity, &SliderValue, &SliderRange), With>, + children: Query<&Children>, + mut thumbs: Query<(&mut Node, Has), Without>, +) { + if let Ok((slider_ent, value, range)) = sliders.get(trigger.target().unwrap()) { + for child in children.iter_descendants(slider_ent) { + if let Ok((mut thumb_node, is_thumb)) = thumbs.get_mut(child) { + if is_thumb { + thumb_node.left = Val::Percent(range.thumb_position(value.0) * 100.0); + } + } + } + } +} + +fn thumb_color(disabled: bool, hovered: bool) -> Color { + match (disabled, hovered) { + (true, _) => GRAY.into(), + + (false, true) => SLIDER_THUMB.lighter(0.3), + + _ => SLIDER_THUMB, + } +} + +/// Update the widget states based on the changing resource. +fn update_widget_values( + res: Res, + mut sliders: Query>, + mut commands: Commands, +) { + if res.is_changed() { + for slider_ent in sliders.iter_mut() { + commands + .entity(slider_ent) + .insert(SliderValue(res.slider_value)); + } + } +} + fn setup(mut commands: Commands, assets: Res) { + // System to print a value when the button is clicked. let on_click = commands.register_system(|| { info!("Button clicked!"); }); + + // System to update a resource when the slider value changes. Note that we could have + // updated the slider value directly, but we want to demonstrate externalizing the state. + let on_change_value = commands.register_system( + |value: In, mut widget_states: ResMut| { + widget_states.slider_value = *value; + }, + ); + // ui camera commands.spawn(Camera2d); - commands.spawn(button(&assets, on_click)); + commands.spawn(demo_root(&assets, on_click, on_change_value)); } -fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { +fn demo_root( + asset_server: &AssetServer, + on_click: SystemId, + on_change_value: SystemId>, +) -> impl Bundle { ( Node { width: Val::Percent(100.0), @@ -245,57 +398,133 @@ fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { justify_content: JustifyContent::Center, display: Display::Flex, flex_direction: FlexDirection::Column, + row_gap: Val::Px(10.0), ..default() }, TabGroup::default(), children![ - ( + button(asset_server, on_click), + slider(0.0, 100.0, 50.0, Some(on_change_value)), + Text::new("Press 'D' to toggle widget disabled states"), + ], + ) +} + +fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle { + ( + Node { + width: Val::Px(150.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + DemoButton, + CoreButton { + on_click: Some(on_click), + }, + Hovered::default(), + TabIndex(0), + BorderColor::all(Color::BLACK), + BorderRadius::MAX, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Button"), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + TextShadow::default(), + )], + ) +} + +/// Create a demo slider +fn slider(min: f32, max: f32, value: f32, on_change: Option>>) -> impl Bundle { + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Stretch, + justify_items: JustifyItems::Center, + column_gap: Val::Px(4.0), + height: Val::Px(12.0), + width: Val::Percent(30.0), + ..default() + }, + Name::new("Slider"), + Hovered::default(), + DemoSlider, + CoreSlider { + on_change, + ..default() + }, + SliderValue(value), + SliderRange::new(min, max), + TabIndex(0), + Children::spawn(( + // Slider background rail + Spawn(( Node { - width: Val::Px(150.0), - height: Val::Px(65.0), - border: UiRect::all(Val::Px(5.0)), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, + height: Val::Px(6.0), ..default() }, - DemoButton, - CoreButton { - on_click: Some(on_click), + BackgroundColor(SLIDER_TRACK), // Border color for the checkbox + BorderRadius::all(Val::Px(3.0)), + )), + // Invisible track to allow absolute placement of thumb entity. This is narrower than + // the actual slider, which allows us to position the thumb entity using simple + // percentages, without having to measure the actual width of the slider thumb. + Spawn(( + Node { + display: Display::Flex, + position_type: PositionType::Absolute, + left: Val::Px(0.0), + // Track is short by 12px to accommodate the thumb. + right: Val::Px(12.0), + top: Val::Px(0.0), + bottom: Val::Px(0.0), + ..default() }, - Hovered::default(), - TabIndex(0), - BorderColor::all(Color::BLACK), - BorderRadius::MAX, - BackgroundColor(NORMAL_BUTTON), children![( - Text::new("Button"), - TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 33.0, + // Thumb + DemoSliderThumb, + CoreSliderThumb, + Node { + display: Display::Flex, + width: Val::Px(12.0), + height: Val::Px(12.0), + position_type: PositionType::Absolute, + left: Val::Percent(0.0), // This will be updated by the slider's value ..default() }, - TextColor(Color::srgb(0.9, 0.9, 0.9)), - TextShadow::default(), - )] - ), - Text::new("Press 'D' to toggle button disabled state"), - ], + BorderRadius::MAX, + BackgroundColor(SLIDER_THUMB), + )], + )), + )), ) } fn toggle_disabled( input: Res>, - mut interaction_query: Query<(Entity, Has), With>, + mut interaction_query: Query< + (Entity, Has), + Or<(With, With)>, + >, mut commands: Commands, ) { if input.just_pressed(KeyCode::KeyD) { for (entity, disabled) in &mut interaction_query { - // disabled.0 = !disabled.0; if disabled { - info!("Button enabled"); + info!("Widgets enabled"); commands.entity(entity).remove::(); } else { - info!("Button disabled"); + info!("Widgets disabled"); commands.entity(entity).insert(InteractionDisabled); } } diff --git a/release-content/release-notes/headless-widgets.md b/release-content/release-notes/headless-widgets.md index 5d39ffe755..b65cae4119 100644 --- a/release-content/release-notes/headless-widgets.md +++ b/release-content/release-notes/headless-widgets.md @@ -1,7 +1,7 @@ --- title: Headless Widgets authors: ["@viridia"] -pull_requests: [19366] +pull_requests: [19366, 19584] --- Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately @@ -33,7 +33,7 @@ The `bevy_core_widgets` crate provides implementations of unstyled widgets, such sliders, checkboxes and radio buttons. - `CoreButton` is a push button. It emits an activation event when clicked. -- (More to be added in subsequent PRs) +- `CoreSlider` is a standard slider, which lets you edit an `f32` value in a given range. ## Widget Interaction States @@ -63,13 +63,13 @@ is using Bevy observers. This approach is useful in cases where you want the wid to bubble up the hierarchy. However, in UI work it's often desirable to connect widget interactions in ways that cut across the -hierarchy. For these kinds of situations, the core widgets offer an an alternate approach: one-shot +hierarchy. For these kinds of situations, the core widgets offer a different approach: one-shot systems. You can register a function as a one-shot system and get the resulting `SystemId`. This can then be passed as a parameter to the widget when it is constructed, so when the button subsequently gets clicked or the slider is dragged, the system gets run. Because it's an ECS system, it can inject any additional parameters it needs to update the Bevy world in response to the interaction. -Most of the core widgets use "external state management" - something that is referred to in the +Most of the core widgets support "external state management" - something that is referred to in the React.js world as "controlled" widgets. This means that for widgets that edit a parameter value (such as checkboxes and sliders), the widget doesn't automatically update its own internal value, but only sends a notification to the app telling it that the value needs to change. It's the @@ -83,6 +83,10 @@ interacting with that widget. Externalizing the state avoids the need for two-wa instead allows simpler one-way data binding that aligns well with the traditional "Model / View / Controller" (MVC) design pattern. +That being said, the choice of internal or external state management is up to you: if the widget +has an `on_change` callback that is not `None`, then the callback is used. If the callback +is `None`, however, the widget will update its own state. (This is similar to how React.js does it.) + There are two exceptions to this rule about external state management. First, widgets which don't edit a value, but which merely trigger an event (such as buttons), don't fall under this rule. Second, widgets which have complex states that are too large and heavyweight to fit within a From 29cfad63a1014ce578b72b7d38a6c2651052d650 Mon Sep 17 00:00:00 2001 From: Trent <2771466+tbillington@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:49:54 +1000 Subject: [PATCH 025/106] Fix typos (#19651) Minor typo fix --- .../src/relationship/relationship_source_collection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/relationship/relationship_source_collection.rs b/crates/bevy_ecs/src/relationship/relationship_source_collection.rs index d4ea45f64f..01c9edf41b 100644 --- a/crates/bevy_ecs/src/relationship/relationship_source_collection.rs +++ b/crates/bevy_ecs/src/relationship/relationship_source_collection.rs @@ -86,13 +86,13 @@ pub trait OrderedRelationshipSourceCollection: RelationshipSourceCollection { /// Inserts the entity at a specific index. /// If the index is too large, the entity will be added to the end of the collection. fn insert(&mut self, index: usize, entity: Entity); - /// Removes the entity at the specified idnex if it exists. + /// Removes the entity at the specified index if it exists. fn remove_at(&mut self, index: usize) -> Option; /// Inserts the entity at a specific index. /// This will never reorder other entities. /// If the index is too large, the entity will be added to the end of the collection. fn insert_stable(&mut self, index: usize, entity: Entity); - /// Removes the entity at the specified idnex if it exists. + /// Removes the entity at the specified index if it exists. /// This will never reorder other entities. fn remove_at_stable(&mut self, index: usize) -> Option; /// Sorts the source collection. From 6ecaf9de534ec9d735688b84ddcc613728bf11d9 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Sun, 15 Jun 2025 09:30:08 -0700 Subject: [PATCH 026/106] Add SkipDeferredLighting component (#19628) Adds a new component for when you want to run the deferred gbuffer prepass, but not the lighting pass. This will be used by bevy_solari in the future, as it'll do it's own shading pass, but still wants the gbuffer. --- crates/bevy_pbr/src/deferred/mod.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index 65be474e65..28edd38c52 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -449,6 +449,7 @@ pub fn prepare_deferred_lighting_pipelines( ), Has>, Has>, + Has, )>, ) { for ( @@ -461,12 +462,13 @@ pub fn prepare_deferred_lighting_pipelines( (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), has_environment_maps, has_irradiance_volumes, + skip_deferred_lighting, ) in &views { - // If there is no deferred prepass, remove the old pipeline if there was - // one. This handles the case in which a view using deferred stops using - // it. - if !deferred_prepass { + // If there is no deferred prepass or we want to skip the deferred lighting pass, + // remove the old pipeline if there was one. This handles the case in which a + // view using deferred stops using it. + if !deferred_prepass || skip_deferred_lighting { commands.entity(entity).remove::(); continue; } @@ -552,3 +554,14 @@ pub fn prepare_deferred_lighting_pipelines( .insert(DeferredLightingPipeline { pipeline_id }); } } + +/// Component to skip running the deferred lighting pass in [`DeferredOpaquePass3dPbrLightingNode`] for a specific view. +/// +/// This works like [`crate::PbrPlugin::add_default_deferred_lighting_plugin`], but is per-view instead of global. +/// +/// Useful for cases where you want to generate a gbuffer, but skip the built-in deferred lighting pass +/// to run your own custom lighting pass instead. +/// +/// Insert this component in the render world only. +#[derive(Component, Clone, Copy, Default)] +pub struct SkipDeferredLighting; From 8bce3e46f276015448947fe3de9bced58cbe252f Mon Sep 17 00:00:00 2001 From: urben1680 <55257931+urben1680@users.noreply.github.com> Date: Sun, 15 Jun 2025 18:41:28 +0200 Subject: [PATCH 027/106] Preserve extra data in `RelationshipTarget` with `replace_related`(`_with_difference`) (#19588) # Objective The methods and commands `replace_related` and `replace_related_with_difference` may cause data stored at the `RelationshipTarget` be lost when all original children are removed before new children are added. Part of https://github.com/bevyengine/bevy/issues/19589 ## Solution Fix the issue, either by removing the old children _after_ adding the new ones and not _before_ (`replace_related_with_difference`) or by taking the whole `RelationshipTarget` to modify it, not only the inner collection (`replace_related`). ## Testing I added a new test asserting the data is kept. I also added a general test of these methods as they had none previously. --------- Co-authored-by: Alice Cecile --- .../src/relationship/related_methods.rs | 119 ++++++++++++------ 1 file changed, 84 insertions(+), 35 deletions(-) diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index fc6d1f1183..1983b6b37c 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -139,22 +139,26 @@ impl<'w> EntityWorldMut<'w> { return self; } - let Some(mut existing_relations) = self.get_mut::() else { + let Some(existing_relations) = self.get_mut::() else { return self.add_related::(related); }; - // We take the collection here so we can modify it without taking the component itself (this would create archetype move). + // We replace the component here with a dummy value so we can modify it without taking it (this would create archetype move). // SAFETY: We eventually return the correctly initialized collection into the target. - let mut existing_relations = mem::replace( - existing_relations.collection_mut_risky(), - Collection::::with_capacity(0), + let mut relations = mem::replace( + existing_relations.into_inner(), + ::RelationshipTarget::from_collection_risky( + Collection::::with_capacity(0), + ), ); + let collection = relations.collection_mut_risky(); + let mut potential_relations = EntityHashSet::from_iter(related.iter().copied()); let id = self.id(); self.world_scope(|world| { - for related in existing_relations.iter() { + for related in collection.iter() { if !potential_relations.remove(related) { world.entity_mut(related).remove::(); } @@ -169,11 +173,9 @@ impl<'w> EntityWorldMut<'w> { }); // SAFETY: The entities we're inserting will be the entities that were either already there or entities that we've just inserted. - existing_relations.clear(); - existing_relations.extend_from_iter(related.iter().copied()); - self.insert(R::RelationshipTarget::from_collection_risky( - existing_relations, - )); + collection.clear(); + collection.extend_from_iter(related.iter().copied()); + self.insert(relations); self } @@ -239,11 +241,20 @@ impl<'w> EntityWorldMut<'w> { assert_eq!(newly_related_entities, entities_to_relate, "`entities_to_relate` ({entities_to_relate:?}) didn't contain all entities that would end up related"); }; - if !self.contains::() { - self.add_related::(entities_to_relate); + match self.get_mut::() { + None => { + self.add_related::(entities_to_relate); - return self; - }; + return self; + } + Some(mut target) => { + // SAFETY: The invariants expected by this function mean we'll only be inserting entities that are already related. + let collection = target.collection_mut_risky(); + collection.clear(); + + collection.extend_from_iter(entities_to_relate.iter().copied()); + } + } let this = self.id(); self.world_scope(|world| { @@ -252,32 +263,13 @@ impl<'w> EntityWorldMut<'w> { } for new_relation in newly_related_entities { - // We're changing the target collection manually so don't run the insert hook + // We changed the target collection manually so don't run the insert hook world .entity_mut(*new_relation) .insert_with_relationship_hook_mode(R::from(this), RelationshipHookMode::Skip); } }); - if !entities_to_relate.is_empty() { - if let Some(mut target) = self.get_mut::() { - // SAFETY: The invariants expected by this function mean we'll only be inserting entities that are already related. - let collection = target.collection_mut_risky(); - collection.clear(); - - collection.extend_from_iter(entities_to_relate.iter().copied()); - } else { - let mut empty = - ::Collection::with_capacity( - entities_to_relate.len(), - ); - empty.extend_from_iter(entities_to_relate.iter().copied()); - - // SAFETY: We've just initialized this collection and we know there's no `RelationshipTarget` on `self` - self.insert(R::RelationshipTarget::from_collection_risky(empty)); - } - } - self } @@ -668,4 +660,61 @@ mod tests { assert_eq!(world.entity(b).get::(), None); assert_eq!(world.entity(c).get::(), None); } + + #[test] + fn replace_related_works() { + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let child3 = world.spawn_empty().id(); + + let mut parent = world.spawn_empty(); + parent.add_children(&[child1, child2]); + let child_value = ChildOf(parent.id()); + let some_child = Some(&child_value); + + parent.replace_children(&[child2, child3]); + let children = parent.get::().unwrap().collection(); + assert_eq!(children, &[child2, child3]); + assert_eq!(parent.world().get::(child1), None); + assert_eq!(parent.world().get::(child2), some_child); + assert_eq!(parent.world().get::(child3), some_child); + + parent.replace_children_with_difference(&[child3], &[child1, child2], &[child1]); + let children = parent.get::().unwrap().collection(); + assert_eq!(children, &[child1, child2]); + assert_eq!(parent.world().get::(child1), some_child); + assert_eq!(parent.world().get::(child2), some_child); + assert_eq!(parent.world().get::(child3), None); + } + + #[test] + fn replace_related_keeps_data() { + #[derive(Component)] + #[relationship(relationship_target = Parent)] + pub struct Child(Entity); + + #[derive(Component)] + #[relationship_target(relationship = Child)] + pub struct Parent { + #[relationship] + children: Vec, + pub data: u8, + } + + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let mut parent = world.spawn_empty(); + parent.add_related::(&[child1]); + parent.get_mut::().unwrap().data = 42; + + parent.replace_related_with_difference::(&[child1], &[child2], &[child2]); + let data = parent.get::().unwrap().data; + assert_eq!(data, 42); + + parent.replace_related::(&[child1]); + let data = parent.get::().unwrap().data; + assert_eq!(data, 42); + } } From 98c14e5917ff4d1cef119a2cc17bf150eaf7887e Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 15 Jun 2025 09:42:54 -0700 Subject: [PATCH 028/106] Replace `UntypedHandle` from ReflectAsset with `impl Into`. (#19606) # Objective - A step towards #19024. - Allow `ReflectAsset` to work with any `AssetId` not just `Handle`. - `ReflectAsset::ids()` returns an iterator of `AssetId`s, but then there's no way to use these ids, since all the other APIs in `ReflectAsset` require a handle (and we don't have a reflect way to get the handle). ## Solution - Replace the `UntypedHandle` argument in `ReflectAsset` methods with `impl Into`. - This matches the regular asset API. - This allows `ReflectAsset::ids()` to be more useful. ## Testing - None. --- crates/bevy_asset/src/reflect.rs | 77 +++++++++++-------- .../reflect_asset_asset_ids.md | 25 ++++++ 2 files changed, 68 insertions(+), 34 deletions(-) create mode 100644 release-content/migration-guides/reflect_asset_asset_ids.md 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/release-content/migration-guides/reflect_asset_asset_ids.md b/release-content/migration-guides/reflect_asset_asset_ids.md new file mode 100644 index 0000000000..cfb52f720f --- /dev/null +++ b/release-content/migration-guides/reflect_asset_asset_ids.md @@ -0,0 +1,25 @@ +--- +title: `ReflectAsset` now uses `UntypedAssetId` instead of `UntypedHandle`. +pull_requests: [19606] +--- + +Previously, `ReflectAsset` methods all required having `UntypedHandle`. The only way to get an +`UntypedHandle` through this API was with `ReflectAsset::add`. `ReflectAsset::ids` was not very +useful in this regard. + +Now, all methods have been changed to accept `impl Into`, which matches our regular +`Assets` API. This means you may need to change how you are calling these methods. + +For example, if your code previously looked like: + +```rust +let my_handle: UntypedHandle; +let my_asset = reflect_asset.get_mut(world, my_handle).unwrap(); +``` + +You can migrate it to: + +```rust +let my_handle: UntypedHandle; +let my_asset = reflect_asset.get_mut(world, &my_handle).unwrap(); +``` From 38c3423693f7d6f3bfe71d8732c246b270b61a4f Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sun, 15 Jun 2025 19:46:34 +0300 Subject: [PATCH 029/106] Event Split: `Event`, `EntityEvent`, and `BufferedEvent` (#19647) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Closes #19564. The current `Event` trait looks like this: ```rust pub trait Event: Send + Sync + 'static { type Traversal: Traversal; const AUTO_PROPAGATE: bool = false; fn register_component_id(world: &mut World) -> ComponentId { ... } fn component_id(world: &World) -> Option { ... } } ``` The `Event` trait is used by both buffered events (`EventReader`/`EventWriter`) and observer events. If they are observer events, they can optionally be targeted at specific `Entity`s or `ComponentId`s, and can even be propagated to other entities. However, there has long been a desire to split the trait semantically for a variety of reasons, see #14843, #14272, and #16031 for discussion. Some reasons include: - It's very uncommon to use a single event type as both a buffered event and targeted observer event. They are used differently and tend to have distinct semantics. - A common footgun is using buffered events with observers or event readers with observer events, as there is no type-level error that prevents this kind of misuse. - #19440 made `Trigger::target` return an `Option`. This *seriously* hurts ergonomics for the general case of entity observers, as you need to `.unwrap()` each time. If we could statically determine whether the event is expected to have an entity target, this would be unnecessary. There's really two main ways that we can categorize events: push vs. pull (i.e. "observer event" vs. "buffered event") and global vs. targeted: | | Push | Pull | | ------------ | --------------- | --------------------------- | | **Global** | Global observer | `EventReader`/`EventWriter` | | **Targeted** | Entity observer | - | There are many ways to approach this, each with their tradeoffs. Ultimately, we kind of want to split events both ways: - A type-level distinction between observer events and buffered events, to prevent people from using the wrong kind of event in APIs - A statically designated entity target for observer events to avoid accidentally using untargeted events for targeted APIs This PR achieves these goals by splitting event traits into `Event`, `EntityEvent`, and `BufferedEvent`, with `Event` being the shared trait implemented by all events. ## `Event`, `EntityEvent`, and `BufferedEvent` `Event` is now a very simple trait shared by all events. ```rust pub trait Event: Send + Sync + 'static { // Required for observer APIs fn register_component_id(world: &mut World) -> ComponentId { ... } fn component_id(world: &World) -> Option { ... } } ``` You can call `trigger` for *any* event, and use a global observer for listening to the event. ```rust #[derive(Event)] struct Speak { message: String, } // ... app.add_observer(|trigger: On| { println!("{}", trigger.message); }); // ... commands.trigger(Speak { message: "Y'all like these reworked events?".to_string(), }); ``` To allow an event to be targeted at entities and even propagated further, you can additionally implement the `EntityEvent` trait: ```rust pub trait EntityEvent: Event { type Traversal: Traversal; const AUTO_PROPAGATE: bool = false; } ``` This lets you call `trigger_targets`, and to use targeted observer APIs like `EntityCommands::observe`: ```rust #[derive(Event, EntityEvent)] #[entity_event(traversal = &'static ChildOf, auto_propagate)] struct Damage { amount: f32, } // ... let enemy = commands.spawn((Enemy, Health(100.0))).id(); // Spawn some armor as a child of the enemy entity. // When the armor takes damage, it will bubble the event up to the enemy. let armor_piece = commands .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) .observe(|trigger: On, mut query: Query<&mut Health>| { // Note: `On::target` only exists because this is an `EntityEvent`. let mut health = query.get(trigger.target()).unwrap(); health.0 -= trigger.amount(); }); commands.trigger_targets(Damage { amount: 10.0 }, armor_piece); ``` > [!NOTE] > You *can* still also trigger an `EntityEvent` without targets using `trigger`. We probably *could* make this an either-or thing, but I'm not sure that's actually desirable. To allow an event to be used with the buffered API, you can implement `BufferedEvent`: ```rust pub trait BufferedEvent: Event {} ``` The event can then be used with `EventReader`/`EventWriter`: ```rust #[derive(Event, BufferedEvent)] struct Message(String); fn write_hello(mut writer: EventWriter) { writer.write(Message("I hope these examples are alright".to_string())); } fn read_messages(mut reader: EventReader) {     // Process all buffered events of type `Message`.   for Message(message) in reader.read() {         println!("{message}");   } } ``` In summary: - Need a basic event you can trigger and observe? Derive `Event`! - Need the event to be targeted at an entity? Derive `EntityEvent`! - Need the event to be buffered and support the `EventReader`/`EventWriter` API? Derive `BufferedEvent`! ## Alternatives I'll now cover some of the alternative approaches I have considered and briefly explored. I made this section collapsible since it ended up being quite long :P