diff --git a/Cargo.toml b/Cargo.toml index 000bebe134..703877983c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3543,6 +3543,17 @@ description = "Illustrates how to use 9 Slicing for TextureAtlases in UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_transform" +path = "examples/ui/ui_transform.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_transform] +name = "UI Transform" +description = "An example demonstrating how to translate, rotate and scale UI elements." +category = "UI (User Interface)" +wasm = true + [[example]] name = "viewport_debug" path = "examples/ui/viewport_debug.rs" diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index ba283d39b2..ade3866b5d 100644 --- a/crates/bevy_ecs/README.md +++ b/crates/bevy_ecs/README.md @@ -340,8 +340,8 @@ let mut world = World::new(); let entity = world.spawn_empty().id(); world.add_observer(|trigger: Trigger, mut commands: Commands| { - println!("Entity {} goes BOOM!", trigger.target()); - commands.entity(trigger.target()).despawn(); + println!("Entity {} goes BOOM!", trigger.target().unwrap()); + commands.entity(trigger.target().unwrap()).despawn(); }); world.flush(); diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 114aff642b..133e5e9eaa 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -1,4 +1,5 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +//! Macros for deriving ECS traits. + #![cfg_attr(docsrs, feature(doc_auto_cfg))] extern crate proc_macro; @@ -29,6 +30,7 @@ enum BundleFieldKind { const BUNDLE_ATTRIBUTE_NAME: &str = "bundle"; const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore"; +/// Implement the `Bundle` trait. #[proc_macro_derive(Bundle, attributes(bundle))] pub fn derive_bundle(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); @@ -187,6 +189,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { }) } +/// Implement the `MapEntities` trait. #[proc_macro_derive(MapEntities, attributes(entities))] pub fn derive_map_entities(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); @@ -522,16 +525,19 @@ pub(crate) fn bevy_ecs_path() -> syn::Path { BevyManifest::shared().get_path("bevy_ecs") } +/// Implement the `Event` trait. #[proc_macro_derive(Event, attributes(event))] pub fn derive_event(input: TokenStream) -> TokenStream { component::derive_event(input) } +/// Implement the `Resource` trait. #[proc_macro_derive(Resource)] pub fn derive_resource(input: TokenStream) -> TokenStream { component::derive_resource(input) } +/// Implement the `Component` trait. #[proc_macro_derive( Component, attributes(component, require, relationship, relationship_target, entities) @@ -540,6 +546,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { component::derive_component(input) } +/// Implement the `FromWorld` trait. #[proc_macro_derive(FromWorld, attributes(from_world))] pub fn derive_from_world(input: TokenStream) -> TokenStream { let bevy_ecs_path = bevy_ecs_path(); diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index f0641ff80c..d7eb707543 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -1143,7 +1143,7 @@ impl<'w> BundleInserter<'w> { if archetype.has_replace_observer() { deferred_world.trigger_observers( ON_REPLACE, - entity, + Some(entity), archetype_after_insert.iter_existing(), caller, ); @@ -1328,7 +1328,7 @@ impl<'w> BundleInserter<'w> { if new_archetype.has_add_observer() { deferred_world.trigger_observers( ON_ADD, - entity, + Some(entity), archetype_after_insert.iter_added(), caller, ); @@ -1346,7 +1346,7 @@ impl<'w> BundleInserter<'w> { if new_archetype.has_insert_observer() { deferred_world.trigger_observers( ON_INSERT, - entity, + Some(entity), archetype_after_insert.iter_inserted(), caller, ); @@ -1365,7 +1365,7 @@ impl<'w> BundleInserter<'w> { if new_archetype.has_insert_observer() { deferred_world.trigger_observers( ON_INSERT, - entity, + Some(entity), archetype_after_insert.iter_added(), caller, ); @@ -1519,7 +1519,7 @@ impl<'w> BundleRemover<'w> { if self.old_archetype.as_ref().has_replace_observer() { deferred_world.trigger_observers( ON_REPLACE, - entity, + Some(entity), bundle_components_in_archetype(), caller, ); @@ -1534,7 +1534,7 @@ impl<'w> BundleRemover<'w> { if self.old_archetype.as_ref().has_remove_observer() { deferred_world.trigger_observers( ON_REMOVE, - entity, + Some(entity), bundle_components_in_archetype(), caller, ); @@ -1785,7 +1785,7 @@ impl<'w> BundleSpawner<'w> { if archetype.has_add_observer() { deferred_world.trigger_observers( ON_ADD, - entity, + Some(entity), bundle_info.iter_contributed_components(), caller, ); @@ -1800,7 +1800,7 @@ impl<'w> BundleSpawner<'w> { if archetype.has_insert_observer() { deferred_world.trigger_observers( ON_INSERT, - entity, + Some(entity), bundle_info.iter_contributed_components(), caller, ); diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 85219d44ca..83bee583d7 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -898,63 +898,39 @@ impl_debug!(Ref<'w, T>,); /// Unique mutable borrow of an entity's component or of a resource. /// -/// This can be used in queries to opt into change detection on both their mutable and immutable forms, as opposed to -/// `&mut T`, which only provides access to change detection while in its mutable form: +/// This can be used in queries to access change detection from immutable query methods, as opposed +/// to `&mut T` which only provides access to change detection from mutable query methods. /// /// ```rust /// # use bevy_ecs::prelude::*; /// # use bevy_ecs::query::QueryData; /// # -/// #[derive(Component, Clone)] +/// #[derive(Component, Clone, Debug)] /// struct Name(String); /// -/// #[derive(Component, Clone, Copy)] +/// #[derive(Component, Clone, Copy, Debug)] /// struct Health(f32); /// -/// #[derive(Component, Clone, Copy)] -/// struct Position { -/// x: f32, -/// y: f32, -/// }; +/// fn my_system(mut query: Query<(Mut, &mut Health)>) { +/// // Mutable access provides change detection information for both parameters: +/// // - `name` has type `Mut` +/// // - `health` has type `Mut` +/// for (name, health) in query.iter_mut() { +/// println!("Name: {:?} (last changed {:?})", name, name.last_changed()); +/// println!("Health: {:?} (last changed: {:?})", health, health.last_changed()); +/// # println!("{}{}", name.0, health.0); // Silence dead_code warning +/// } /// -/// #[derive(Component, Clone, Copy)] -/// struct Player { -/// id: usize, -/// }; -/// -/// #[derive(QueryData)] -/// #[query_data(mutable)] -/// struct PlayerQuery { -/// id: &'static Player, -/// -/// // Reacting to `PlayerName` changes is expensive, so we need to enable change detection when reading it. -/// name: Mut<'static, Name>, -/// -/// health: &'static mut Health, -/// position: &'static mut Position, -/// } -/// -/// fn update_player_avatars(players_query: Query) { -/// // The item returned by the iterator is of type `PlayerQueryReadOnlyItem`. -/// for player in players_query.iter() { -/// if player.name.is_changed() { -/// // Update the player's name. This clones a String, and so is more expensive. -/// update_player_name(player.id, player.name.clone()); -/// } -/// -/// // Update the health bar. -/// update_player_health(player.id, *player.health); -/// -/// // Update the player's position. -/// update_player_position(player.id, *player.position); +/// // Immutable access only provides change detection for `Name`: +/// // - `name` has type `Ref` +/// // - `health` has type `&Health` +/// for (name, health) in query.iter() { +/// println!("Name: {:?} (last changed {:?})", name, name.last_changed()); +/// println!("Health: {:?}", health); /// } /// } /// -/// # bevy_ecs::system::assert_is_system(update_player_avatars); -/// -/// # fn update_player_name(player: &Player, new_name: Name) {} -/// # fn update_player_health(player: &Player, new_health: Health) {} -/// # fn update_player_position(player: &Player, new_position: Position) {} +/// # bevy_ecs::system::assert_is_system(my_system); /// ``` pub struct Mut<'w, T: ?Sized> { pub(crate) value: &'w mut T, diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 767dc7ec95..ed5a8b176f 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -68,7 +68,7 @@ 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 [`Entity::PLACEHOLDER`]. + /// 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 @@ -81,7 +81,7 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { /// /// 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) -> Entity { + pub fn target(&self) -> Option { self.trigger.target } @@ -341,7 +341,7 @@ pub struct ObserverTrigger { /// The [`ComponentId`]s the trigger targeted. components: SmallVec<[ComponentId; 2]>, /// The entity the trigger targeted. - pub target: Entity, + pub target: Option, /// The location of the source code that triggered the observer. pub caller: MaybeLocation, } @@ -416,7 +416,7 @@ impl Observers { pub(crate) fn invoke( mut world: DeferredWorld, event_type: ComponentId, - target: Entity, + target: Option, components: impl Iterator + Clone, data: &mut T, propagate: &mut bool, @@ -455,8 +455,8 @@ impl Observers { observers.map.iter().for_each(&mut trigger_observer); // Trigger entity observers listening for this kind of trigger - if target != Entity::PLACEHOLDER { - if let Some(map) = observers.entity_observers.get(&target) { + if let Some(target_entity) = target { + if let Some(map) = observers.entity_observers.get(&target_entity) { map.iter().for_each(&mut trigger_observer); } } @@ -469,8 +469,8 @@ impl Observers { .iter() .for_each(&mut trigger_observer); - if target != Entity::PLACEHOLDER { - if let Some(map) = component_observers.entity_map.get(&target) { + if let Some(target_entity) = target { + if let Some(map) = component_observers.entity_map.get(&target_entity) { map.iter().for_each(&mut trigger_observer); } } @@ -695,7 +695,7 @@ impl World { unsafe { world.trigger_observers_with_data::<_, E::Traversal>( event_id, - Entity::PLACEHOLDER, + None, targets.components(), event_data, false, @@ -708,7 +708,7 @@ impl World { unsafe { world.trigger_observers_with_data::<_, E::Traversal>( event_id, - target_entity, + Some(target_entity), targets.components(), event_data, E::AUTO_PROPAGATE, @@ -999,20 +999,20 @@ mod tests { world.add_observer( |obs: Trigger, mut res: ResMut, mut commands: Commands| { res.observed("add_a"); - commands.entity(obs.target()).insert(B); + commands.entity(obs.target().unwrap()).insert(B); }, ); world.add_observer( |obs: Trigger, mut res: ResMut, mut commands: Commands| { res.observed("remove_a"); - commands.entity(obs.target()).remove::(); + commands.entity(obs.target().unwrap()).remove::(); }, ); world.add_observer( |obs: Trigger, mut res: ResMut, mut commands: Commands| { res.observed("add_b"); - commands.entity(obs.target()).remove::(); + commands.entity(obs.target().unwrap()).remove::(); }, ); world.add_observer(|_: Trigger, mut res: ResMut| { @@ -1181,7 +1181,7 @@ mod tests { }; world.spawn_empty().observe(system); world.add_observer(move |obs: Trigger, mut res: ResMut| { - assert_eq!(obs.target(), Entity::PLACEHOLDER); + assert_eq!(obs.target(), None); res.observed("event_a"); }); @@ -1208,7 +1208,7 @@ mod tests { .observe(|_: Trigger, mut res: ResMut| res.observed("a_1")) .id(); world.add_observer(move |obs: Trigger, mut res: ResMut| { - assert_eq!(obs.target(), entity); + assert_eq!(obs.target().unwrap(), entity); res.observed("a_2"); }); @@ -1628,7 +1628,7 @@ mod tests { world.add_observer( |trigger: Trigger, query: Query<&A>, mut res: ResMut| { - if query.get(trigger.target()).is_ok() { + if query.get(trigger.target().unwrap()).is_ok() { res.observed("event"); } }, @@ -1651,7 +1651,7 @@ mod tests { fn observer_modifies_relationship() { fn on_add(trigger: Trigger, mut commands: Commands) { commands - .entity(trigger.target()) + .entity(trigger.target().unwrap()) .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 43ece18ff5..c3acff2e6e 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -123,8 +123,8 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// struct Explode; /// /// world.add_observer(|trigger: Trigger, mut commands: Commands| { -/// println!("Entity {} goes BOOM!", trigger.target()); -/// commands.entity(trigger.target()).despawn(); +/// println!("Entity {} goes BOOM!", trigger.target().unwrap()); +/// commands.entity(trigger.target().unwrap()).despawn(); /// }); /// /// world.flush(); @@ -157,7 +157,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # struct Explode; /// world.entity_mut(e1).observe(|trigger: Trigger, mut commands: Commands| { /// println!("Boom!"); -/// commands.entity(trigger.target()).despawn(); +/// commands.entity(trigger.target().unwrap()).despawn(); /// }); /// /// world.entity_mut(e2).observe(|trigger: Trigger, mut commands: Commands| { diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index cffba8cda1..55fa42c41e 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -47,6 +47,8 @@ use variadics_please::all_tuples; /// - **[`Ref`].** /// Similar to change detection filters but it is used as a query fetch parameter. /// It exposes methods to check for changes to the wrapped component. +/// - **[`Mut`].** +/// Mutable component access, with change detection data. /// - **[`Has`].** /// Returns a bool indicating whether the entity has the specified component. /// diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index 5a23214463..fc6d1f1183 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -1,6 +1,7 @@ use crate::{ bundle::Bundle, entity::{hash_set::EntityHashSet, Entity}, + prelude::Children, relationship::{ Relationship, RelationshipHookMode, RelationshipSourceCollection, RelationshipTarget, }, @@ -302,6 +303,15 @@ impl<'w> EntityWorldMut<'w> { self } + /// Despawns the children of this entity. + /// This entity will not be despawned. + /// + /// This is a specialization of [`despawn_related`](EntityWorldMut::despawn_related), a more general method for despawning via relationships. + pub fn despawn_children(&mut self) -> &mut Self { + self.despawn_related::(); + self + } + /// Inserts a component or bundle of components into the entity and all related entities, /// traversing the relationship tracked in `S` in a breadth-first manner. /// @@ -467,6 +477,14 @@ impl<'a> EntityCommands<'a> { }) } + /// Despawns the children of this entity. + /// This entity will not be despawned. + /// + /// This is a specialization of [`despawn_related`](EntityCommands::despawn_related), a more general method for despawning via relationships. + pub fn despawn_children(&mut self) -> &mut Self { + self.despawn_related::() + } + /// Inserts a component or bundle of components into the entity and all related entities, /// traversing the relationship tracked in `S` in a breadth-first manner. /// diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index cb6f3dcf8f..b73ffdfd33 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -20,7 +20,7 @@ use crate::{ prelude::{IntoSystemSet, SystemSet}, query::{Access, FilteredAccessSet}, schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet}, - system::{ScheduleSystem, System, SystemIn, SystemParamValidationError}, + system::{ScheduleSystem, System, SystemIn, SystemParamValidationError, SystemStateFlags}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; @@ -171,26 +171,9 @@ impl System for ApplyDeferred { const { &FilteredAccessSet::new() } } - fn is_send(&self) -> bool { - // Although this system itself does nothing on its own, the system - // executor uses it to apply deferred commands. Commands must be allowed - // to access non-send resources, so this system must be non-send for - // scheduling purposes. - false - } - - fn is_exclusive(&self) -> bool { - // This system is labeled exclusive because it is used by the system - // executor to find places where deferred commands should be applied, - // and commands can only be applied with exclusive access to the world. - true - } - - fn has_deferred(&self) -> bool { - // This system itself doesn't have any commands to apply, but when it - // is pulled from the schedule to be ran, the executor will apply - // deferred commands from other systems. - false + fn flags(&self) -> SystemStateFlags { + // non-send , exclusive , no deferred + SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE } unsafe fn run_unsafe( diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 81912d2f72..807112f438 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -874,7 +874,6 @@ mod tests { } #[test] - #[ignore = "Known failing but fix is non-trivial: https://github.com/bevyengine/bevy/issues/4381"] fn filtered_components() { let mut world = World::new(); world.spawn(A); diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 144ce6516c..2d559db1dd 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -1418,26 +1418,24 @@ impl ScheduleGraph { if system_a.is_exclusive() || system_b.is_exclusive() { conflicting_systems.push((a, b, Vec::new())); } else { - let access_a = system_a.component_access(); - let access_b = system_b.component_access(); - if !access_a.is_compatible(access_b) { - match access_a.get_conflicts(access_b) { - AccessConflicts::Individual(conflicts) => { - let conflicts: Vec<_> = conflicts - .ones() - .map(ComponentId::get_sparse_set_index) - .filter(|id| !ignored_ambiguities.contains(id)) - .collect(); - if !conflicts.is_empty() { - conflicting_systems.push((a, b, conflicts)); - } - } - AccessConflicts::All => { - // there is no specific component conflicting, but the systems are overall incompatible - // for example 2 systems with `Query` - conflicting_systems.push((a, b, Vec::new())); + let access_a = system_a.component_access_set(); + let access_b = system_b.component_access_set(); + match access_a.get_conflicts(access_b) { + AccessConflicts::Individual(conflicts) => { + let conflicts: Vec<_> = conflicts + .ones() + .map(ComponentId::get_sparse_set_index) + .filter(|id| !ignored_ambiguities.contains(id)) + .collect(); + if !conflicts.is_empty() { + conflicting_systems.push((a, b, conflicts)); } } + AccessConflicts::All => { + // there is no specific component conflicting, but the systems are overall incompatible + // for example 2 systems with `Query` + conflicting_systems.push((a, b, Vec::new())); + } } } } diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index f728a4e907..062fbb3d5b 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -137,16 +137,9 @@ where self.system.component_access_set() } - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + #[inline] + fn flags(&self) -> super::SystemStateFlags { + self.system.flags() } #[inline] diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 29a87e93ce..18de141263 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -152,16 +152,9 @@ where &self.component_access_set } - fn is_send(&self) -> bool { - self.a.is_send() && self.b.is_send() - } - - fn is_exclusive(&self) -> bool { - self.a.is_exclusive() || self.b.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.a.has_deferred() || self.b.has_deferred() + #[inline] + fn flags(&self) -> super::SystemStateFlags { + self.a.flags() | self.b.flags() } unsafe fn run_unsafe( @@ -378,16 +371,9 @@ where &self.component_access_set } - fn is_send(&self) -> bool { - self.a.is_send() && self.b.is_send() - } - - fn is_exclusive(&self) -> bool { - self.a.is_exclusive() || self.b.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.a.has_deferred() || self.b.has_deferred() + #[inline] + fn flags(&self) -> super::SystemStateFlags { + self.a.flags() | self.b.flags() } unsafe fn run_unsafe( diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 0920fd1e1f..f7e362a670 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -13,7 +13,7 @@ use alloc::{borrow::Cow, vec, vec::Vec}; use core::marker::PhantomData; use variadics_please::all_tuples; -use super::SystemParamValidationError; +use super::{SystemParamValidationError, SystemStateFlags}; /// A function system that runs with exclusive [`World`] access. /// @@ -98,22 +98,12 @@ where } #[inline] - fn is_send(&self) -> bool { - // exclusive systems should have access to non-send resources + fn flags(&self) -> SystemStateFlags { + // non-send , exclusive , no deferred // the executor runs exclusive systems on the main thread, so this // field reflects that constraint - false - } - - #[inline] - fn is_exclusive(&self) -> bool { - true - } - - #[inline] - fn has_deferred(&self) -> bool { // exclusive systems have no deferred system params - false + SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE } #[inline] diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index af26e81d2f..d257a9f079 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -17,7 +17,9 @@ use variadics_please::all_tuples; #[cfg(feature = "trace")] use tracing::{info_span, Span}; -use super::{IntoSystem, ReadOnlySystem, SystemParamBuilder, SystemParamValidationError}; +use super::{ + IntoSystem, ReadOnlySystem, SystemParamBuilder, SystemParamValidationError, SystemStateFlags, +}; /// The metadata of a [`System`]. #[derive(Clone)] @@ -29,8 +31,7 @@ pub struct SystemMeta { pub(crate) component_access_set: FilteredAccessSet, // NOTE: this must be kept private. making a SystemMeta non-send is irreversible to prevent // SystemParams from overriding each other - is_send: bool, - has_deferred: bool, + flags: SystemStateFlags, pub(crate) last_run: Tick, #[cfg(feature = "trace")] pub(crate) system_span: Span, @@ -44,8 +45,7 @@ impl SystemMeta { Self { name: name.into(), component_access_set: FilteredAccessSet::default(), - is_send: true, - has_deferred: false, + flags: SystemStateFlags::empty(), last_run: Tick::new(0), #[cfg(feature = "trace")] system_span: info_span!("system", name = name), @@ -78,7 +78,7 @@ impl SystemMeta { /// Returns true if the system is [`Send`]. #[inline] pub fn is_send(&self) -> bool { - self.is_send + !self.flags.intersects(SystemStateFlags::NON_SEND) } /// Sets the system to be not [`Send`]. @@ -86,20 +86,20 @@ impl SystemMeta { /// This is irreversible. #[inline] pub fn set_non_send(&mut self) { - self.is_send = false; + self.flags |= SystemStateFlags::NON_SEND; } /// Returns true if the system has deferred [`SystemParam`]'s #[inline] pub fn has_deferred(&self) -> bool { - self.has_deferred + self.flags.intersects(SystemStateFlags::DEFERRED) } /// Marks the system as having deferred buffers like [`Commands`](`super::Commands`) /// This lets the scheduler insert [`ApplyDeferred`](`crate::prelude::ApplyDeferred`) systems automatically. #[inline] pub fn set_has_deferred(&mut self) { - self.has_deferred = true; + self.flags |= SystemStateFlags::DEFERRED; } /// Returns a reference to the [`FilteredAccessSet`] for [`ComponentId`]. @@ -631,18 +631,8 @@ where } #[inline] - fn is_send(&self) -> bool { - self.system_meta.is_send - } - - #[inline] - fn is_exclusive(&self) -> bool { - false - } - - #[inline] - fn has_deferred(&self) -> bool { - self.system_meta.has_deferred + fn flags(&self) -> SystemStateFlags { + self.system_meta.flags } #[inline] diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index c0ac5e8de0..b123301a08 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -127,18 +127,8 @@ where } #[inline] - fn is_send(&self) -> bool { - self.observer.is_send() - } - - #[inline] - fn is_exclusive(&self) -> bool { - self.observer.is_exclusive() - } - - #[inline] - fn has_deferred(&self) -> bool { - self.observer.has_deferred() + fn flags(&self) -> super::SystemStateFlags { + self.observer.flags() } #[inline] diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index f7e6de9803..a8d6ecf8a8 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -2016,17 +2016,67 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { self.as_nop().get(entity).is_ok() } - /// Returns a [`QueryLens`] that can be used to get a query with a more general fetch. + /// Returns a [`QueryLens`] that can be used to construct a new [`Query`] giving more + /// restrictive access to the entities matched by the current query. /// - /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. - /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), - /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be - /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] + /// A transmute is valid only if `NewD` has a subset of the read, write, and required access + /// of the current query. A precise description of the access required by each parameter + /// type is given in the table below, but typical uses are to: + /// * Remove components, e.g. `Query<(&A, &B)>` to `Query<&A>`. + /// * Retrieve an existing component with reduced or equal access, e.g. `Query<&mut A>` to `Query<&A>` + /// or `Query<&T>` to `Query>`. + /// * Add parameters with no new access, for example adding an `Entity` parameter. + /// + /// Note that since filter terms are dropped, non-archetypal filters like + /// [`Added`], [`Changed`] and [`Spawned`] will not be respected. To maintain or change filter + /// terms see [`Self::transmute_lens_filtered`]. + /// + /// |`QueryData` parameter type|Access required| + /// |----|----| + /// |[`Entity`], [`EntityLocation`], [`SpawnDetails`], [`&Archetype`], [`Has`], [`PhantomData`]|No access| + /// |[`EntityMut`]|Read and write access to all components, but no required access| + /// |[`EntityRef`]|Read access to all components, but no required access| + /// |`&T`, [`Ref`]|Read and required access to `T`| + /// |`&mut T`, [`Mut`]|Read, write and required access to `T`| + /// |[`Option`], [`AnyOf<(D, ...)>`]|Read and write access to `T`, but no required access| + /// |Tuples of query data and
`#[derive(QueryData)]` structs|The union of the access of their subqueries| + /// |[`FilteredEntityRef`], [`FilteredEntityMut`]|Determined by the [`QueryBuilder`] used to construct them. Any query can be transmuted to them, and they will receive the access of the source query. When combined with other `QueryData`, they will receive any access of the source query that does not conflict with the other data| + /// + /// `transmute_lens` drops filter terms, but [`Self::transmute_lens_filtered`] supports returning a [`QueryLens`] with a new + /// filter type - the access required by filter parameters are as follows. + /// + /// |`QueryFilter` parameter type|Access required| + /// |----|----| + /// |[`Added`], [`Changed`]|Read and required access to `T`| + /// |[`With`], [`Without`]|No access| + /// |[`Or<(T, ...)>`]|Read access of the subqueries, but no required access| + /// |Tuples of query filters and `#[derive(QueryFilter)]` structs|The union of the access of their subqueries| + /// + /// [`Added`]: crate::query::Added + /// [`Added`]: crate::query::Added + /// [`AnyOf<(D, ...)>`]: crate::query::AnyOf + /// [`&Archetype`]: crate::archetype::Archetype + /// [`Changed`]: crate::query::Changed + /// [`Changed`]: crate::query::Changed + /// [`EntityMut`]: crate::world::EntityMut + /// [`EntityLocation`]: crate::entity::EntityLocation + /// [`EntityRef`]: crate::world::EntityRef + /// [`FilteredEntityRef`]: crate::world::FilteredEntityRef + /// [`FilteredEntityMut`]: crate::world::FilteredEntityMut + /// [`Has`]: crate::query::Has + /// [`Mut`]: crate::world::Mut + /// [`Or<(T, ...)>`]: crate::query::Or + /// [`QueryBuilder`]: crate::query::QueryBuilder + /// [`Ref`]: crate::world::Ref + /// [`SpawnDetails`]: crate::query::SpawnDetails + /// [`Spawned`]: crate::query::Spawned + /// [`With`]: crate::query::With + /// [`Without`]: crate::query::Without /// /// ## Panics /// - /// This will panic if `NewD` is not a subset of the original fetch `D` + /// This will panic if the access required by `NewD` is not a subset of that required by + /// the original fetch `D`. /// /// ## Example /// @@ -2065,30 +2115,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// # schedule.run(&mut world); /// ``` /// - /// ## Allowed Transmutes - /// - /// Besides removing parameters from the query, - /// you can also make limited changes to the types of parameters. - /// The new query must have a subset of the *read*, *write*, and *required* access of the original query. - /// - /// * `&mut T` and [`Mut`](crate::change_detection::Mut) have read, write, and required access to `T` - /// * `&T` and [`Ref`](crate::change_detection::Ref) have read and required access to `T` - /// * [`Option`] and [`AnyOf<(D, ...)>`](crate::query::AnyOf) have the read and write access of the subqueries, but no required access - /// * Tuples of query data and `#[derive(QueryData)]` structs have the union of the access of their subqueries - /// * [`EntityMut`](crate::world::EntityMut) has read and write access to all components, but no required access - /// * [`EntityRef`](crate::world::EntityRef) has read access to all components, but no required access - /// * [`Entity`], [`EntityLocation`], [`SpawnDetails`], [`&Archetype`], [`Has`], and [`PhantomData`] have no access at all, - /// so can be added to any query - /// * [`FilteredEntityRef`](crate::world::FilteredEntityRef) and [`FilteredEntityMut`](crate::world::FilteredEntityMut) - /// have access determined by the [`QueryBuilder`](crate::query::QueryBuilder) used to construct them. - /// Any query can be transmuted to them, and they will receive the access of the source query. - /// When combined with other `QueryData`, they will receive any access of the source query that does not conflict with the other data. - /// * [`Added`](crate::query::Added) and [`Changed`](crate::query::Changed) filters have read and required access to `T` - /// * [`With`](crate::query::With) and [`Without`](crate::query::Without) filters have no access at all, - /// so can be added to any query - /// * Tuples of query filters and `#[derive(QueryFilter)]` structs have the union of the access of their subqueries - /// * [`Or<(F, ...)>`](crate::query::Or) filters have the read access of the subqueries, but no required access - /// /// ### Examples of valid transmutes /// /// ```rust @@ -2165,28 +2191,21 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// // Nested inside of an `Or` filter, they have the same access as `Option<&T>`. /// assert_valid_transmute_filtered::, (), Entity, Or<(Changed, With)>>(); /// ``` - /// - /// [`EntityLocation`]: crate::entity::EntityLocation - /// [`SpawnDetails`]: crate::query::SpawnDetails - /// [`&Archetype`]: crate::archetype::Archetype - /// [`Has`]: crate::query::Has #[track_caller] pub fn transmute_lens(&mut self) -> QueryLens<'_, NewD> { self.transmute_lens_filtered::() } - /// Returns a [`QueryLens`] that can be used to get a query with a more general fetch. + /// Returns a [`QueryLens`] that can be used to construct a new `Query` giving more restrictive + /// access to the entities matched by the current query. + /// /// This consumes the [`Query`] to return results with the actual "inner" world lifetime. /// - /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. - /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), - /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be - /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] + /// See [`Self::transmute_lens`] for a description of allowed transmutes. /// /// ## Panics /// - /// This will panic if `NewD` is not a subset of the original fetch `Q` + /// This will panic if `NewD` is not a subset of the original fetch `D` /// /// ## Example /// @@ -2225,22 +2244,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// # schedule.run(&mut world); /// ``` /// - /// ## Allowed Transmutes - /// - /// Besides removing parameters from the query, you can also - /// make limited changes to the types of parameters. - /// - /// * Can always add/remove [`Entity`] - /// * Can always add/remove [`EntityLocation`] - /// * Can always add/remove [`&Archetype`] - /// * `Ref` <-> `&T` - /// * `&mut T` -> `&T` - /// * `&mut T` -> `Ref` - /// * [`EntityMut`](crate::world::EntityMut) -> [`EntityRef`](crate::world::EntityRef) - /// - /// [`EntityLocation`]: crate::entity::EntityLocation - /// [`&Archetype`]: crate::archetype::Archetype - /// /// # See also /// /// - [`transmute_lens`](Self::transmute_lens) to convert to a lens using a mutable borrow of the [`Query`]. @@ -2251,6 +2254,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Equivalent to [`Self::transmute_lens`] but also includes a [`QueryFilter`] type. /// + /// See [`Self::transmute_lens`] for a description of allowed transmutes. + /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), @@ -2266,10 +2271,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Equivalent to [`Self::transmute_lens_inner`] but also includes a [`QueryFilter`] type. /// This consumes the [`Query`] to return results with the actual "inner" world lifetime. /// + /// See [`Self::transmute_lens`] for a description of allowed transmutes. + /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will only be respected if they + /// are in the type signature. /// /// # See also /// diff --git a/crates/bevy_ecs/src/system/schedule_system.rs b/crates/bevy_ecs/src/system/schedule_system.rs index 26fdbdbe0f..17bd3f46de 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -8,7 +8,7 @@ use crate::{ world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FromWorld, World}, }; -use super::{IntoSystem, SystemParamValidationError}; +use super::{IntoSystem, SystemParamValidationError, SystemStateFlags}; /// A wrapper system to change a system that returns `()` to return `Ok(())` to make it into a [`ScheduleSystem`] pub struct InfallibleSystemWrapper>(S); @@ -44,18 +44,8 @@ impl> System for InfallibleSystemWrapper { } #[inline] - fn is_send(&self) -> bool { - self.0.is_send() - } - - #[inline] - fn is_exclusive(&self) -> bool { - self.0.is_exclusive() - } - - #[inline] - fn has_deferred(&self) -> bool { - self.0.has_deferred() + fn flags(&self) -> SystemStateFlags { + self.0.flags() } #[inline] @@ -172,16 +162,9 @@ where self.system.component_access_set() } - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + #[inline] + fn flags(&self) -> SystemStateFlags { + self.system.flags() } unsafe fn run_unsafe( @@ -281,16 +264,9 @@ where self.system.component_access_set() } - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + #[inline] + fn flags(&self) -> SystemStateFlags { + self.system.flags() } unsafe fn run_unsafe( diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index edb2ead9cd..211152f3d8 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -2,6 +2,7 @@ clippy::module_inception, reason = "This instance of module inception is being discussed; see #17353." )] +use bitflags::bitflags; use core::fmt::Debug; use log::warn; use thiserror::Error; @@ -19,6 +20,18 @@ use core::any::TypeId; use super::{IntoSystem, SystemParamValidationError}; +bitflags! { + /// Bitflags representing system states and requirements. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct SystemStateFlags: u8 { + /// Set if system cannot be sent across threads + const NON_SEND = 1 << 0; + /// Set if system requires exclusive World access + const EXCLUSIVE = 1 << 1; + /// Set if system has deferred buffers. + const DEFERRED = 1 << 2; + } +} /// An ECS system that can be added to a [`Schedule`](crate::schedule::Schedule) /// /// Systems are functions with all arguments implementing @@ -50,14 +63,26 @@ pub trait System: Send + Sync + 'static { /// Returns the system's component [`FilteredAccessSet`]. fn component_access_set(&self) -> &FilteredAccessSet; + /// Returns the [`SystemStateFlags`] of the system. + fn flags(&self) -> SystemStateFlags; + /// Returns true if the system is [`Send`]. - fn is_send(&self) -> bool; + #[inline] + fn is_send(&self) -> bool { + !self.flags().intersects(SystemStateFlags::NON_SEND) + } /// Returns true if the system must be run exclusively. - fn is_exclusive(&self) -> bool; + #[inline] + fn is_exclusive(&self) -> bool { + self.flags().intersects(SystemStateFlags::EXCLUSIVE) + } /// Returns true if system has deferred buffers. - fn has_deferred(&self) -> bool; + #[inline] + fn has_deferred(&self) -> bool { + self.flags().intersects(SystemStateFlags::DEFERRED) + } /// Runs the system with the given input in the world. Unlike [`System::run`], this function /// can be called in parallel with other systems and may break Rust's aliasing rules diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 02c12fe6a3..5a20046b2f 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -23,7 +23,7 @@ use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World, ON_INSERT, ON_REPLAC /// /// This means that in order to add entities, for example, you will need to use commands instead of the world directly. pub struct DeferredWorld<'w> { - // SAFETY: Implementors must not use this reference to make structural changes + // SAFETY: Implementers must not use this reference to make structural changes world: UnsafeWorldCell<'w>, } @@ -157,7 +157,7 @@ impl<'w> DeferredWorld<'w> { if archetype.has_replace_observer() { self.trigger_observers( ON_REPLACE, - entity, + Some(entity), [component_id].into_iter(), MaybeLocation::caller(), ); @@ -197,7 +197,7 @@ impl<'w> DeferredWorld<'w> { if archetype.has_insert_observer() { self.trigger_observers( ON_INSERT, - entity, + Some(entity), [component_id].into_iter(), MaybeLocation::caller(), ); @@ -738,7 +738,7 @@ impl<'w> DeferredWorld<'w> { pub(crate) unsafe fn trigger_observers( &mut self, event: ComponentId, - target: Entity, + target: Option, components: impl Iterator + Clone, caller: MaybeLocation, ) { @@ -761,7 +761,7 @@ impl<'w> DeferredWorld<'w> { pub(crate) unsafe fn trigger_observers_with_data( &mut self, event: ComponentId, - mut target: Entity, + target: Option, components: impl Iterator + Clone, data: &mut E, mut propagate: bool, @@ -769,18 +769,20 @@ impl<'w> DeferredWorld<'w> { ) where T: Traversal, { + Observers::invoke::<_>( + self.reborrow(), + event, + target, + components.clone(), + data, + &mut propagate, + caller, + ); + let Some(mut target) = target else { return }; + loop { - Observers::invoke::<_>( - self.reborrow(), - event, - target, - components.clone(), - data, - &mut propagate, - caller, - ); if !propagate { - break; + return; } if let Some(traverse_to) = self .get_entity(target) @@ -792,6 +794,15 @@ impl<'w> DeferredWorld<'w> { } else { break; } + Observers::invoke::<_>( + self.reborrow(), + event, + Some(target), + components.clone(), + data, + &mut propagate, + caller, + ); } } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 1f67865df5..dd653831d9 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2371,7 +2371,7 @@ impl<'w> EntityWorldMut<'w> { if archetype.has_despawn_observer() { deferred_world.trigger_observers( ON_DESPAWN, - self.entity, + Some(self.entity), archetype.components(), caller, ); @@ -2385,7 +2385,7 @@ impl<'w> EntityWorldMut<'w> { if archetype.has_replace_observer() { deferred_world.trigger_observers( ON_REPLACE, - self.entity, + Some(self.entity), archetype.components(), caller, ); @@ -2400,7 +2400,7 @@ impl<'w> EntityWorldMut<'w> { if archetype.has_remove_observer() { deferred_world.trigger_observers( ON_REMOVE, - self.entity, + Some(self.entity), archetype.components(), caller, ); @@ -5749,7 +5749,9 @@ mod tests { let entity = world .spawn_empty() .observe(|trigger: Trigger, mut commands: Commands| { - commands.entity(trigger.target()).insert(TestComponent(0)); + commands + .entity(trigger.target().unwrap()) + .insert(TestComponent(0)); }) .id(); @@ -5769,7 +5771,7 @@ mod tests { let mut world = World::new(); world.add_observer( |trigger: Trigger, mut commands: Commands| { - commands.entity(trigger.target()).despawn(); + commands.entity(trigger.target().unwrap()).despawn(); }, ); let entity = world.spawn_empty().id(); diff --git a/crates/bevy_image/src/image_loader.rs b/crates/bevy_image/src/image_loader.rs index 0ef1213b46..fe086db674 100644 --- a/crates/bevy_image/src/image_loader.rs +++ b/crates/bevy_image/src/image_loader.rs @@ -81,19 +81,35 @@ impl ImageLoader { } } +/// How to determine an image's format when loading. #[derive(Serialize, Deserialize, Default, Debug, Clone)] pub enum ImageFormatSetting { + /// Determine the image format from its file extension. + /// + /// This is the default. #[default] FromExtension, + /// Declare the image format explicitly. Format(ImageFormat), + /// Guess the image format by looking for magic bytes at the + /// beginning of its data. Guess, } +/// Settings for loading an [`Image`] using an [`ImageLoader`]. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ImageLoaderSettings { + /// How to determine the image's format. pub format: ImageFormatSetting, + /// Specifies whether image data is linear + /// or in sRGB space when this is not determined by + /// the image format. pub is_srgb: bool, + /// [`ImageSampler`] to use when rendering - this does + /// not affect the loading of the image data. pub sampler: ImageSampler, + /// Where the asset will be used - see the docs on + /// [`RenderAssetUsages`] for details. pub asset_usage: RenderAssetUsages, } @@ -108,11 +124,14 @@ impl Default for ImageLoaderSettings { } } +/// An error when loading an image using [`ImageLoader`]. #[non_exhaustive] #[derive(Debug, Error)] pub enum ImageLoaderError { - #[error("Could load shader: {0}")] + /// An error occurred while trying to load the image bytes. + #[error("Failed to load image bytes: {0}")] Io(#[from] std::io::Error), + /// An error occurred while trying to decode the image bytes. #[error("Could not load texture file: {0}")] FileTexture(#[from] FileTextureError), } @@ -170,7 +189,7 @@ impl AssetLoader for ImageLoader { /// An error that occurs when loading a texture from a file. #[derive(Error, Debug)] -#[error("Error reading image file {path}: {error}, this is an error in `bevy_render`.")] +#[error("Error reading image file {path}: {error}.")] pub struct FileTextureError { error: TextureError, path: String, diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index da3a7bc8e9..e8879d7a9c 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -394,7 +394,7 @@ mod tests { trigger: Trigger>, mut query: Query<&mut GatherKeyboardEvents>, ) { - if let Ok(mut gather) = query.get_mut(trigger.target()) { + if let Ok(mut gather) = query.get_mut(trigger.target().unwrap()) { 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 e779dbc841..2592936ddd 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: Trigger, mut commands: Commands, ) { - if let Ok(mut v) = commands.get_entity(trigger.target()) { + if let Ok(mut v) = commands.get_entity(trigger.target().unwrap()) { v.insert(LightViewEntities::default()); } } @@ -561,7 +561,7 @@ pub(crate) fn extracted_light_removed( trigger: Trigger, mut commands: Commands, ) { - if let Ok(mut v) = commands.get_entity(trigger.target()) { + if let Ok(mut v) = commands.get_entity(trigger.target().unwrap()) { 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()) { + if let Ok(entities) = query.get(trigger.target().unwrap()) { 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/hover.rs b/crates/bevy_picking/src/hover.rs index 6347568c02..2bf23c50ba 100644 --- a/crates/bevy_picking/src/hover.rs +++ b/crates/bevy_picking/src/hover.rs @@ -208,18 +208,6 @@ pub fn update_interactions( mut pointers: Query<(&PointerId, &PointerPress, &mut PointerInteraction)>, mut interact: Query<&mut PickingInteraction>, ) { - // Clear all previous hover data from pointers and entities - for (pointer, _, mut pointer_interaction) in &mut pointers { - pointer_interaction.sorted_entities.clear(); - if let Some(previously_hovered_entities) = previous_hover_map.get(pointer) { - for entity in previously_hovered_entities.keys() { - if let Ok(mut interaction) = interact.get_mut(*entity) { - *interaction = PickingInteraction::None; - } - } - } - } - // Create a map to hold the aggregated interaction for each entity. This is needed because we // need to be able to insert the interaction component on entities if they do not exist. To do // so we need to know the final aggregated interaction state to avoid the scenario where we set @@ -239,13 +227,29 @@ pub fn update_interactions( } // Take the aggregated entity states and update or insert the component if missing. - for (hovered_entity, new_interaction) in new_interaction_state.drain() { + for (&hovered_entity, &new_interaction) in new_interaction_state.iter() { if let Ok(mut interaction) = interact.get_mut(hovered_entity) { - *interaction = new_interaction; + interaction.set_if_neq(new_interaction); } else if let Ok(mut entity_commands) = commands.get_entity(hovered_entity) { entity_commands.try_insert(new_interaction); } } + + // Clear all previous hover data from pointers that are no longer hovering any entities. + // We do this last to preserve change detection for picking interactions. + for (pointer, _, _) in &mut pointers { + let Some(previously_hovered_entities) = previous_hover_map.get(pointer) else { + continue; + }; + + for entity in previously_hovered_entities.keys() { + if !new_interaction_state.contains_key(entity) { + if let Ok(mut interaction) = interact.get_mut(*entity) { + interaction.set_if_neq(PickingInteraction::None); + } + } + } + } } /// Merge the interaction state of this entity into the aggregated map. diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 53387e84c8..70a5714581 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -55,13 +55,13 @@ //! // Spawn your entity here, e.g. a Mesh. //! // When dragged, mutate the `Transform` component on the dragged target entity: //! .observe(|trigger: Trigger>, mut transforms: Query<&mut Transform>| { -//! let mut transform = transforms.get_mut(trigger.target()).unwrap(); +//! 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| { -//! println!("Entity {} goes BOOM!", trigger.target()); -//! commands.entity(trigger.target()).despawn(); +//! println!("Entity {} goes BOOM!", trigger.target().unwrap()); +//! commands.entity(trigger.target().unwrap()).despawn(); //! }) //! .observe(|trigger: Trigger>, mut events: EventWriter| { //! events.write(Greeting); diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index e844d950be..d9775e9c8f 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -119,7 +119,7 @@ wesl = { version = "0.1.2", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Omit the `glsl` feature in non-WebAssembly by default. -naga_oil = { version = "0.17", default-features = false, features = [ +naga_oil = { version = "0.17.1", default-features = false, features = [ "test_shader", ] } @@ -127,7 +127,7 @@ naga_oil = { version = "0.17", default-features = false, features = [ proptest = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] -naga_oil = "0.17" +naga_oil = "0.17.1" js-sys = "0.3" web-sys = { version = "0.3.67", features = [ 'Blob', diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index f19f57fa5f..d97950240d 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -1060,6 +1060,7 @@ pub fn camera_system( #[reflect(opaque)] #[reflect(Component, Default, Clone)] pub struct CameraMainTextureUsages(pub TextureUsages); + impl Default for CameraMainTextureUsages { fn default() -> Self { Self( @@ -1070,6 +1071,13 @@ impl Default for CameraMainTextureUsages { } } +impl CameraMainTextureUsages { + pub fn with(mut self, usages: TextureUsages) -> Self { + self.0 |= usages; + self + } +} + #[derive(Component, Debug)] pub struct ExtractedCamera { pub target: Option, diff --git a/crates/bevy_render/src/render_resource/bind_group_entries.rs b/crates/bevy_render/src/render_resource/bind_group_entries.rs index 3aaf46183f..cc8eb188de 100644 --- a/crates/bevy_render/src/render_resource/bind_group_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_entries.rs @@ -147,6 +147,13 @@ impl<'a> IntoBinding<'a> for &'a TextureView { } } +impl<'a> IntoBinding<'a> for &'a wgpu::TextureView { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureView(self) + } +} + impl<'a> IntoBinding<'a> for &'a [&'a wgpu::TextureView] { #[inline] fn into_binding(self) -> BindingResource<'a> { @@ -161,6 +168,13 @@ impl<'a> IntoBinding<'a> for &'a Sampler { } } +impl<'a> IntoBinding<'a> for &'a [&'a wgpu::Sampler] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::SamplerArray(self) + } +} + impl<'a> IntoBinding<'a> for BindingResource<'a> { #[inline] fn into_binding(self) -> BindingResource<'a> { @@ -175,6 +189,13 @@ impl<'a> IntoBinding<'a> for wgpu::BufferBinding<'a> { } } +impl<'a> IntoBinding<'a> for &'a [wgpu::BufferBinding<'a>] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::BufferArray(self) + } +} + pub trait IntoBindingArray<'b, const N: usize> { fn into_array(self) -> [BindingResource<'b>; N]; } diff --git a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs index bc4a7d306d..41affa4349 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs @@ -568,4 +568,8 @@ pub mod binding_types { } .into_bind_group_layout_entry_builder() } + + pub fn acceleration_structure() -> BindGroupLayoutEntryBuilder { + BindingType::AccelerationStructure.into_bind_group_layout_entry_builder() + } } diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index b777d96290..aecf27173d 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -38,18 +38,21 @@ pub use wgpu::{ BufferInitDescriptor, DispatchIndirectArgs, DrawIndexedIndirectArgs, DrawIndirectArgs, TextureDataOrder, }, - AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, AstcChannel, BindGroupDescriptor, - BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, + AccelerationStructureFlags, AccelerationStructureGeometryFlags, + AccelerationStructureUpdateMode, AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, + AstcChannel, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingResource, BindingType, Blas, BlasBuildEntry, BlasGeometries, + BlasGeometrySizeDescriptors, BlasTriangleGeometry, BlasTriangleGeometrySizeDescriptor, BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferAsyncError, BufferBinding, BufferBindingType, BufferDescriptor, BufferSize, BufferUsages, ColorTargetState, ColorWrites, CommandEncoder, CommandEncoderDescriptor, CompareFunction, ComputePass, ComputePassDescriptor, ComputePipelineDescriptor as RawComputePipelineDescriptor, - DepthBiasState, DepthStencilState, DownlevelFlags, Extent3d, Face, Features as WgpuFeatures, - FilterMode, FragmentState as RawFragmentState, FrontFace, ImageSubresourceRange, IndexFormat, - Limits as WgpuLimits, LoadOp, Maintain, MapMode, MultisampleState, Operations, Origin3d, - PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PolygonMode, - PrimitiveState, PrimitiveTopology, PushConstantRange, RenderPassColorAttachment, - RenderPassDepthStencilAttachment, RenderPassDescriptor, + CreateBlasDescriptor, CreateTlasDescriptor, DepthBiasState, DepthStencilState, DownlevelFlags, + Extent3d, Face, Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState, + FrontFace, ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, Maintain, MapMode, + MultisampleState, Operations, Origin3d, PipelineCompilationOptions, PipelineLayout, + PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, PushConstantRange, + RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipelineDescriptor as RawRenderPipelineDescriptor, Sampler as WgpuSampler, SamplerBindingType, SamplerBindingType as WgpuSamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState, @@ -57,8 +60,9 @@ pub use wgpu::{ TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureFormatFeatureFlags, TextureFormatFeatures, TextureSampleType, TextureUsages, TextureView as WgpuTextureView, TextureViewDescriptor, - TextureViewDimension, VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, - VertexFormat, VertexState as RawVertexState, VertexStepMode, COPY_BUFFER_ALIGNMENT, + TextureViewDimension, Tlas, TlasInstance, TlasPackage, VertexAttribute, + VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState, + VertexStepMode, COPY_BUFFER_ALIGNMENT, }; pub use crate::mesh::VertexBufferLayout; diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index f6c2f87593..90613451dd 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -94,14 +94,14 @@ impl Plugin for SyncWorldPlugin { app.init_resource::(); app.add_observer( |trigger: Trigger, mut pending: ResMut| { - pending.push(EntityRecord::Added(trigger.target())); + pending.push(EntityRecord::Added(trigger.target().unwrap())); }, ); app.add_observer( |trigger: Trigger, mut pending: ResMut, query: Query<&RenderEntity>| { - if let Ok(e) = query.get(trigger.target()) { + if let Ok(e) = query.get(trigger.target().unwrap()) { pending.push(EntityRecord::Removed(*e)); }; }, @@ -512,14 +512,14 @@ mod tests { main_world.add_observer( |trigger: Trigger, mut pending: ResMut| { - pending.push(EntityRecord::Added(trigger.target())); + pending.push(EntityRecord::Added(trigger.target().unwrap())); }, ); main_world.add_observer( |trigger: Trigger, mut pending: ResMut, query: Query<&RenderEntity>| { - if let Ok(e) = query.get(trigger.target()) { + if let Ok(e) = query.get(trigger.target().unwrap()) { pending.push(EntityRecord::Removed(*e)); }; }, diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 1daa0158b3..456cb62225 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -721,7 +721,7 @@ mod tests { .expect("Failed to run dynamic scene builder system.") } - fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Entity) { + fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Option) { // Add observer app.world_mut().add_observer( move |trigger: Trigger, @@ -773,7 +773,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); + observe_trigger(&mut app, scene_id, None); } #[test] @@ -792,7 +792,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); + observe_trigger(&mut app, scene_id, None); } #[test] @@ -816,7 +816,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, scene_entity); + observe_trigger(&mut app, scene_id, Some(scene_entity)); } #[test] @@ -840,7 +840,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, scene_entity); + observe_trigger(&mut app, scene_id, Some(scene_entity)); } #[test] diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index 637ed943f2..78e80717a2 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -1,6 +1,7 @@ use crate::{ experimental::UiChildren, prelude::{Button, Label}, + ui_transform::UiGlobalTransform, widget::{ImageNode, TextUiReader}, ComputedNode, }; @@ -13,11 +14,9 @@ use bevy_ecs::{ system::{Commands, Query}, world::Ref, }; -use bevy_math::Vec3Swizzles; -use bevy_render::camera::CameraUpdateSystems; -use bevy_transform::prelude::GlobalTransform; use accesskit::{Node, Rect, Role}; +use bevy_render::camera::CameraUpdateSystems; fn calc_label( text_reader: &mut TextUiReader, @@ -40,12 +39,12 @@ fn calc_bounds( mut nodes: Query<( &mut AccessibilityNode, Ref, - Ref, + Ref, )>, ) { for (mut accessible, node, transform) in &mut nodes { if node.is_changed() || transform.is_changed() { - let center = transform.translation().xy(); + let center = transform.translation; let half_size = 0.5 * node.size; let min = center - half_size; let max = center + half_size; diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 9242bf1380..f55cbb92b8 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,18 +1,21 @@ -use crate::{CalculatedClip, ComputedNode, ComputedNodeTarget, ResolvedBorderRadius, UiStack}; +use crate::{ + picking_backend::clip_check_recursive, ui_transform::UiGlobalTransform, ComputedNode, + ComputedNodeTarget, Node, UiStack, +}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::{ContainsEntity, Entity}, + hierarchy::ChildOf, prelude::{Component, With}, query::QueryData, reflect::ReflectComponent, system::{Local, Query, Res}, }; use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput}; -use bevy_math::{Rect, Vec2}; +use bevy_math::Vec2; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::InheritedVisibility}; -use bevy_transform::components::GlobalTransform; use bevy_window::{PrimaryWindow, Window}; use smallvec::SmallVec; @@ -67,12 +70,12 @@ impl Default for Interaction { } } -/// A component storing the position of the mouse relative to the node, (0., 0.) being the top-left corner and (1., 1.) being the bottom-right -/// If the mouse is not over the node, the value will go beyond the range of (0., 0.) to (1., 1.) +/// A component storing the position of the mouse relative to the node, (0., 0.) being the center and (0.5, 0.5) being the bottom-right +/// If the mouse is not over the node, the value will go beyond the range of (-0.5, -0.5) to (0.5, 0.5) /// /// It can be used alongside [`Interaction`] to get the position of the press. /// -/// The component is updated when it is in the same entity with [`Node`](crate::Node). +/// The component is updated when it is in the same entity with [`Node`]. #[derive(Component, Copy, Clone, Default, PartialEq, Debug, Reflect)] #[reflect(Component, Default, PartialEq, Debug, Clone)] #[cfg_attr( @@ -81,8 +84,8 @@ impl Default for Interaction { reflect(Serialize, Deserialize) )] pub struct RelativeCursorPosition { - /// Visible area of the Node relative to the size of the entire Node. - pub normalized_visible_node_rect: Rect, + /// True if the cursor position is over an unclipped area of the Node. + pub cursor_over: bool, /// Cursor position relative to the size and position of the Node. /// A None value indicates that the cursor position is unknown. pub normalized: Option, @@ -90,9 +93,8 @@ pub struct RelativeCursorPosition { impl RelativeCursorPosition { /// A helper function to check if the mouse is over the node - pub fn mouse_over(&self) -> bool { - self.normalized - .is_some_and(|position| self.normalized_visible_node_rect.contains(position)) + pub fn cursor_over(&self) -> bool { + self.cursor_over } } @@ -133,11 +135,10 @@ pub struct State { pub struct NodeQuery { entity: Entity, node: &'static ComputedNode, - global_transform: &'static GlobalTransform, + transform: &'static UiGlobalTransform, interaction: Option<&'static mut Interaction>, relative_cursor_position: Option<&'static mut RelativeCursorPosition>, focus_policy: Option<&'static FocusPolicy>, - calculated_clip: Option<&'static CalculatedClip>, inherited_visibility: Option<&'static InheritedVisibility>, target_camera: &'static ComputedNodeTarget, } @@ -154,6 +155,8 @@ pub fn ui_focus_system( touches_input: Res, ui_stack: Res, mut node_query: Query, + clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: Query<&ChildOf>, ) { let primary_window = primary_window.iter().next(); @@ -234,46 +237,30 @@ pub fn ui_focus_system( } let camera_entity = node.target_camera.camera()?; - let node_rect = Rect::from_center_size( - node.global_transform.translation().truncate(), - node.node.size(), - ); - - // Intersect with the calculated clip rect to find the bounds of the visible region of the node - let visible_rect = node - .calculated_clip - .map(|clip| node_rect.intersect(clip.clip)) - .unwrap_or(node_rect); - let cursor_position = camera_cursor_positions.get(&camera_entity); + let contains_cursor = cursor_position.is_some_and(|point| { + node.node.contains_point(*node.transform, *point) + && clip_check_recursive(*point, *entity, &clipping_query, &child_of_query) + }); + // The mouse position relative to the node - // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner + // (-0.5, -0.5) is the top-left corner, (0.5, 0.5) is the bottom-right corner // Coordinates are relative to the entire node, not just the visible region. - let relative_cursor_position = cursor_position.and_then(|cursor_position| { + let normalized_cursor_position = cursor_position.and_then(|cursor_position| { // ensure node size is non-zero in all dimensions, otherwise relative position will be // +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to // false positives for mouse_over (#12395) - (node_rect.size().cmpgt(Vec2::ZERO).all()) - .then_some((*cursor_position - node_rect.min) / node_rect.size()) + node.node.normalize_point(*node.transform, *cursor_position) }); // If the current cursor position is within the bounds of the node's visible area, consider it for // clicking let relative_cursor_position_component = RelativeCursorPosition { - normalized_visible_node_rect: visible_rect.normalize(node_rect), - normalized: relative_cursor_position, + cursor_over: contains_cursor, + normalized: normalized_cursor_position, }; - let contains_cursor = relative_cursor_position_component.mouse_over() - && cursor_position.is_some_and(|point| { - pick_rounded_rect( - *point - node_rect.center(), - node_rect.size(), - node.node.border_radius, - ) - }); - // Save the relative cursor position to the correct component if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position { @@ -284,7 +271,8 @@ pub fn ui_focus_system( Some(*entity) } else { if let Some(mut interaction) = node.interaction { - if *interaction == Interaction::Hovered || (relative_cursor_position.is_none()) + if *interaction == Interaction::Hovered + || (normalized_cursor_position.is_none()) { interaction.set_if_neq(Interaction::None); } @@ -334,26 +322,3 @@ pub fn ui_focus_system( } } } - -// Returns true if `point` (relative to the rectangle's center) is within the bounds of a rounded rectangle with -// the given size and border radius. -// -// Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. -pub(crate) fn pick_rounded_rect( - point: Vec2, - size: Vec2, - border_radius: ResolvedBorderRadius, -) -> bool { - let [top, bottom] = if point.x < 0. { - [border_radius.top_left, border_radius.bottom_left] - } else { - [border_radius.top_right, border_radius.bottom_right] - }; - let r = if point.y < 0. { top } else { bottom }; - - let corner_to_point = point.abs() - 0.5 * size; - let q = corner_to_point + r; - let l = q.max(Vec2::ZERO).length(); - let m = q.max_element().min(0.); - l + m - r < 0. -} diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 079e73bb49..53c03113b9 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -448,6 +448,8 @@ impl RepeatedGridTrack { #[cfg(test)] mod tests { + use bevy_math::Vec2; + use super::*; #[test] @@ -523,7 +525,7 @@ mod tests { grid_column: GridPlacement::start(4), grid_row: GridPlacement::span(3), }; - let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.)); + let viewport_values = LayoutContext::new(1.0, Vec2::new(800., 600.)); let taffy_style = from_node(&node, &viewport_values, false); assert_eq!(taffy_style.display, taffy::style::Display::Flex); assert_eq!(taffy_style.box_sizing, taffy::style::BoxSizing::ContentBox); @@ -661,7 +663,7 @@ mod tests { #[test] fn test_into_length_percentage() { use taffy::style::LengthPercentage; - let context = LayoutContext::new(2.0, bevy_math::Vec2::new(800., 600.)); + let context = LayoutContext::new(2.0, Vec2::new(800., 600.)); let cases = [ (Val::Auto, LengthPercentage::Length(0.)), (Val::Percent(1.), LengthPercentage::Percent(0.01)), diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index b38241a95a..a5e53d981e 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1,5 +1,6 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, + ui_transform::{UiGlobalTransform, UiTransform}, BorderRadius, ComputedNode, ComputedNodeTarget, ContentSize, Display, LayoutConfig, Node, Outline, OverflowAxis, ScrollPosition, }; @@ -12,9 +13,9 @@ use bevy_ecs::{ system::{Commands, Query, ResMut}, world::Ref, }; -use bevy_math::Vec2; + +use bevy_math::{Affine2, Vec2}; use bevy_sprite::BorderRect; -use bevy_transform::components::Transform; use thiserror::Error; use tracing::warn; use ui_surface::UiSurface; @@ -81,9 +82,10 @@ pub fn ui_layout_system( )>, computed_node_query: Query<(Entity, Option>), With>, ui_children: UiChildren, - mut node_transform_query: Query<( + mut node_update_query: Query<( &mut ComputedNode, - &mut Transform, + &UiTransform, + &mut UiGlobalTransform, &Node, Option<&LayoutConfig>, Option<&BorderRadius>, @@ -175,7 +177,8 @@ with UI components as a child of an entity without UI components, your UI layout &mut ui_surface, true, computed_target.physical_size().as_vec2(), - &mut node_transform_query, + Affine2::IDENTITY, + &mut node_update_query, &ui_children, computed_target.scale_factor.recip(), Vec2::ZERO, @@ -190,9 +193,11 @@ with UI components as a child of an entity without UI components, your UI layout ui_surface: &mut UiSurface, inherited_use_rounding: bool, target_size: Vec2, - node_transform_query: &mut Query<( + mut inherited_transform: Affine2, + node_update_query: &mut Query<( &mut ComputedNode, - &mut Transform, + &UiTransform, + &mut UiGlobalTransform, &Node, Option<&LayoutConfig>, Option<&BorderRadius>, @@ -206,13 +211,14 @@ with UI components as a child of an entity without UI components, your UI layout ) { if let Ok(( mut node, - mut transform, + transform, + mut global_transform, style, maybe_layout_config, maybe_border_radius, maybe_outline, maybe_scroll_position, - )) = node_transform_query.get_mut(entity) + )) = node_update_query.get_mut(entity) { let use_rounding = maybe_layout_config .map(|layout_config| layout_config.use_rounding) @@ -224,10 +230,11 @@ with UI components as a child of an entity without UI components, your UI layout let layout_size = Vec2::new(layout.size.width, layout.size.height); + // Taffy layout position of the top-left corner of the node, relative to its parent. let layout_location = Vec2::new(layout.location.x, layout.location.y); - // The position of the center of the node, stored in the node's transform - let node_center = + // The position of the center of the node relative to its top-left corner. + let local_center = layout_location - parent_scroll_position + 0.5 * (layout_size - parent_size); // only trigger change detection when the new values are different @@ -253,6 +260,16 @@ with UI components as a child of an entity without UI components, your UI layout node.bypass_change_detection().border = taffy_rect_to_border_rect(layout.border); node.bypass_change_detection().padding = taffy_rect_to_border_rect(layout.padding); + // Computer the node's new global transform + let mut local_transform = + transform.compute_affine(inverse_target_scale_factor, layout_size, target_size); + local_transform.translation += local_center; + inherited_transform *= local_transform; + + if inherited_transform != **global_transform { + *global_transform = inherited_transform.into(); + } + if let Some(border_radius) = maybe_border_radius { // We don't trigger change detection for changes to border radius node.bypass_change_detection().border_radius = border_radius.resolve( @@ -290,10 +307,6 @@ with UI components as a child of an entity without UI components, your UI layout .max(0.); } - if transform.translation.truncate() != node_center { - transform.translation = node_center.extend(0.); - } - let scroll_position: Vec2 = maybe_scroll_position .map(|scroll_pos| { Vec2::new( @@ -333,7 +346,8 @@ with UI components as a child of an entity without UI components, your UI layout ui_surface, use_rounding, target_size, - node_transform_query, + inherited_transform, + node_update_query, ui_children, inverse_target_scale_factor, layout_size, @@ -356,10 +370,7 @@ mod tests { use bevy_platform::collections::HashMap; use bevy_render::{camera::ManualTextureViews, prelude::Camera}; use bevy_transform::systems::mark_dirty_trees; - use bevy_transform::{ - prelude::GlobalTransform, - systems::{propagate_parent_transforms, sync_simple_transforms}, - }; + use bevy_transform::systems::{propagate_parent_transforms, sync_simple_transforms}; use bevy_utils::prelude::default; use bevy_window::{ PrimaryWindow, Window, WindowCreated, WindowResized, WindowResolution, @@ -684,23 +695,20 @@ mod tests { ui_schedule.run(&mut world); let overlap_check = world - .query_filtered::<(Entity, &ComputedNode, &GlobalTransform), Without>() + .query_filtered::<(Entity, &ComputedNode, &UiGlobalTransform), Without>() .iter(&world) .fold( Option::<(Rect, bool)>::None, - |option_rect, (entity, node, global_transform)| { - let current_rect = Rect::from_center_size( - global_transform.translation().truncate(), - node.size(), - ); + |option_rect, (entity, node, transform)| { + let current_rect = Rect::from_center_size(transform.translation, node.size()); assert!( current_rect.height().abs() + current_rect.width().abs() > 0., "root ui node {entity} doesn't have a logical size" ); assert_ne!( - global_transform.affine(), - GlobalTransform::default().affine(), - "root ui node {entity} global transform is not populated" + *transform, + UiGlobalTransform::default(), + "root ui node {entity} transform is not populated" ); let Some((rect, is_overlapping)) = option_rect else { return Some((current_rect, false)); diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 4ce6359206..be0649c038 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -18,6 +18,7 @@ pub mod widget; pub mod gradients; #[cfg(feature = "bevy_ui_picking_backend")] pub mod picking_backend; +pub mod ui_transform; use bevy_derive::{Deref, DerefMut}; #[cfg(feature = "bevy_ui_picking_backend")] @@ -42,6 +43,7 @@ pub use measurement::*; pub use render::*; pub use ui_material::*; pub use ui_node::*; +pub use ui_transform::*; use widget::{ImageNode, ImageNodeSize, ViewportNode}; @@ -64,6 +66,7 @@ pub mod prelude { gradients::*, ui_material::*, ui_node::*, + ui_transform::*, widget::{Button, ImageNode, Label, NodeImageMode, ViewportNode}, Interaction, MaterialNode, UiMaterialPlugin, UiScale, }, diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 26b84c6005..5647baee12 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -24,14 +24,13 @@ #![deny(missing_docs)] -use crate::{focus::pick_rounded_rect, prelude::*, UiStack}; +use crate::{prelude::*, ui_transform::UiGlobalTransform, UiStack}; use bevy_app::prelude::*; use bevy_ecs::{prelude::*, query::QueryData}; -use bevy_math::{Rect, Vec2}; +use bevy_math::Vec2; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::prelude::*; -use bevy_transform::prelude::*; use bevy_window::PrimaryWindow; use bevy_picking::backend::prelude::*; @@ -91,9 +90,8 @@ impl Plugin for UiPickingPlugin { pub struct NodeQuery { entity: Entity, node: &'static ComputedNode, - global_transform: &'static GlobalTransform, + transform: &'static UiGlobalTransform, pickable: Option<&'static Pickable>, - calculated_clip: Option<&'static CalculatedClip>, inherited_visibility: Option<&'static InheritedVisibility>, target_camera: &'static ComputedNodeTarget, } @@ -110,6 +108,8 @@ pub fn ui_picking( ui_stack: Res, node_query: Query, mut output: EventWriter, + clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: Query<&ChildOf>, ) { // For each camera, the pointer and its position let mut pointer_pos_by_camera = HashMap::>::default(); @@ -181,43 +181,33 @@ pub fn ui_picking( continue; }; - let node_rect = Rect::from_center_size( - node.global_transform.translation().truncate(), - node.node.size(), - ); - // Nodes with Display::None have a (0., 0.) logical rect and can be ignored - if node_rect.size() == Vec2::ZERO { + if node.node.size() == Vec2::ZERO { continue; } - // Intersect with the calculated clip rect to find the bounds of the visible region of the node - let visible_rect = node - .calculated_clip - .map(|clip| node_rect.intersect(clip.clip)) - .unwrap_or(node_rect); - let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity); - // The mouse position relative to the node - // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner + // Find the normalized cursor position relative to the node. + // (±0., 0.) is the center with the corners at points (±0.5, ±0.5). // Coordinates are relative to the entire node, not just the visible region. for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) { - let relative_cursor_position = (*cursor_position - node_rect.min) / node_rect.size(); - - if visible_rect - .normalize(node_rect) - .contains(relative_cursor_position) - && pick_rounded_rect( - *cursor_position - node_rect.center(), - node_rect.size(), - node.node.border_radius, + if node.node.contains_point(*node.transform, *cursor_position) + && clip_check_recursive( + *cursor_position, + *node_entity, + &clipping_query, + &child_of_query, ) { hit_nodes .entry((camera_entity, *pointer_id)) .or_default() - .push((*node_entity, relative_cursor_position)); + .push(( + *node_entity, + node.transform.inverse().transform_point2(*cursor_position) + / node.node.size(), + )); } } } @@ -262,3 +252,27 @@ pub fn ui_picking( output.write(PointerHits::new(*pointer, picks, order)); } } + +/// Walk up the tree child-to-parent checking that `point` is not clipped by any ancestor node. +pub fn clip_check_recursive( + point: Vec2, + entity: Entity, + clipping_query: &Query<'_, '_, (&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: &Query<&ChildOf>, +) -> bool { + if let Ok(child_of) = child_of_query.get(entity) { + let parent = child_of.0; + if let Ok((computed_node, transform, node)) = clipping_query.get(parent) { + if !computed_node + .resolve_clip_rect(node.overflow, node.overflow_clip_margin) + .contains(transform.inverse().transform_point2(point)) + { + // The point is clipped and should be ignored by picking + return false; + } + } + return clip_check_recursive(point, parent, clipping_query, child_of_query); + } + // Reached root, point unclipped by all ancestors + true +} diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index 1c2b2c7d0a..b6f3f3501e 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -2,6 +2,7 @@ use core::{hash::Hash, ops::Range}; +use crate::prelude::UiGlobalTransform; use crate::{ BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystems, ResolvedBorderRadius, TransparentUi, Val, @@ -18,7 +19,7 @@ use bevy_ecs::{ }, }; use bevy_image::BevyDefault as _; -use bevy_math::{vec2, FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles}; +use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2}; use bevy_render::sync_world::MainEntity; use bevy_render::RenderApp; use bevy_render::{ @@ -29,7 +30,6 @@ use bevy_render::{ view::*, Extract, ExtractSchedule, Render, RenderSystems, }; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; use super::{stack_z_offsets, UiCameraMap, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS}; @@ -211,7 +211,7 @@ impl SpecializedRenderPipeline for BoxShadowPipeline { /// Description of a shadow to be sorted and queued for rendering pub struct ExtractedBoxShadow { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub bounds: Vec2, pub clip: Option, pub extracted_camera_entity: Entity, @@ -236,7 +236,7 @@ pub fn extract_shadows( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, &BoxShadow, Option<&CalculatedClip>, @@ -302,7 +302,7 @@ pub fn extract_shadows( extracted_box_shadows.box_shadows.push(ExtractedBoxShadow { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix() * Mat4::from_translation(offset.extend(0.)), + transform: Affine2::from(transform) * Affine2::from_translation(offset), color: drop_shadow.color.into(), bounds: shadow_size + 6. * blur_radius, clip: clip.map(|clip| clip.clip), @@ -405,11 +405,15 @@ pub fn prepare_shadows( .get(item.index) .filter(|n| item.entity() == n.render_entity) { - let rect_size = box_shadow.bounds.extend(1.0); + let rect_size = box_shadow.bounds; // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (box_shadow.transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + box_shadow + .transform + .transform_point2(pos * rect_size) + .extend(0.) + }); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -443,7 +447,7 @@ pub fn prepare_shadows( positions[3] + positions_diff[3].extend(0.), ]; - let transformed_rect_size = box_shadow.transform.transform_vector3(rect_size); + let transformed_rect_size = box_shadow.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -492,7 +496,7 @@ pub fn prepare_shadows( size: box_shadow.size.into(), radius, blur: box_shadow.blur_radius, - bounds: rect_size.xy().into(), + bounds: rect_size.into(), }); } diff --git a/crates/bevy_ui/src/render/debug_overlay.rs b/crates/bevy_ui/src/render/debug_overlay.rs index aa9440b8d8..c3ada22c2e 100644 --- a/crates/bevy_ui/src/render/debug_overlay.rs +++ b/crates/bevy_ui/src/render/debug_overlay.rs @@ -1,5 +1,6 @@ use crate::shader_flags; use crate::ui_node::ComputedNodeTarget; +use crate::ui_transform::UiGlobalTransform; use crate::CalculatedClip; use crate::ComputedNode; use bevy_asset::AssetId; @@ -16,7 +17,6 @@ use bevy_render::sync_world::TemporaryRenderEntity; use bevy_render::view::InheritedVisibility; use bevy_render::Extract; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; use super::ExtractedUiItem; use super::ExtractedUiNode; @@ -62,9 +62,9 @@ pub fn extract_debug_overlay( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, - &GlobalTransform, &ComputedNodeTarget, )>, >, @@ -76,7 +76,7 @@ pub fn extract_debug_overlay( let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, visibility, maybe_clip, transform, computed_target) in &uinode_query { + for (entity, uinode, transform, visibility, maybe_clip, computed_target) in &uinode_query { if !debug_options.show_hidden && !visibility.get() { continue; } @@ -102,7 +102,7 @@ pub fn extract_debug_overlay( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), diff --git a/crates/bevy_ui/src/render/gradient.rs b/crates/bevy_ui/src/render/gradient.rs index 31369899f7..bd818c7d5b 100644 --- a/crates/bevy_ui/src/render/gradient.rs +++ b/crates/bevy_ui/src/render/gradient.rs @@ -17,8 +17,9 @@ use bevy_ecs::{ use bevy_image::prelude::*; use bevy_math::{ ops::{cos, sin}, - FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles, + FloatOrd, Rect, Vec2, }; +use bevy_math::{Affine2, Vec2Swizzles}; use bevy_render::sync_world::MainEntity; use bevy_render::{ render_phase::*, @@ -29,7 +30,6 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::BorderRect; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; use super::shader_flags::BORDER_ALL; @@ -238,7 +238,7 @@ pub enum ResolvedGradient { pub struct ExtractedGradient { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub clip: Option, pub extracted_camera_entity: Entity, @@ -354,7 +354,7 @@ pub fn extract_gradients( Entity, &ComputedNode, &ComputedNodeTarget, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, AnyOf<(&BackgroundGradient, &BorderGradient)>, @@ -414,7 +414,7 @@ pub fn extract_gradients( border_radius: uinode.border_radius, border: uinode.border, node_type, - transform: transform.compute_matrix(), + transform: transform.into(), }, main_entity: entity.into(), render_entity: commands.spawn(TemporaryRenderEntity).id(), @@ -439,7 +439,7 @@ pub fn extract_gradients( extracted_gradients.items.push(ExtractedGradient { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), stops_range: range_start..extracted_color_stops.0.len(), rect: Rect { min: Vec2::ZERO, @@ -487,7 +487,7 @@ pub fn extract_gradients( extracted_gradients.items.push(ExtractedGradient { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), stops_range: range_start..extracted_color_stops.0.len(), rect: Rect { min: Vec2::ZERO, @@ -541,7 +541,7 @@ pub fn extract_gradients( extracted_gradients.items.push(ExtractedGradient { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), stops_range: range_start..extracted_color_stops.0.len(), rect: Rect { min: Vec2::ZERO, @@ -675,12 +675,16 @@ pub fn prepare_gradient( *item.batch_range_mut() = item_index as u32..item_index as u32 + 1; let uinode_rect = gradient.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (gradient.transform * (pos * rect_size).extend(1.)).xyz()); - let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + gradient + .transform + .transform_point2(pos * rect_size) + .extend(0.) + }); + let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -721,7 +725,7 @@ pub fn prepare_gradient( corner_points[3] + positions_diff[3], ]; - let transformed_rect_size = gradient.transform.transform_vector3(rect_size); + let transformed_rect_size = gradient.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 83140d0f3b..61319eda9b 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -8,7 +8,9 @@ pub mod ui_texture_slice_pipeline; mod debug_overlay; mod gradient; +use crate::prelude::UiGlobalTransform; use crate::widget::{ImageNode, ViewportNode}; + use crate::{ BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, @@ -22,7 +24,7 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_ecs::prelude::*; use bevy_ecs::system::SystemParam; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Mat4, Rect, UVec4, Vec2}; use bevy_render::load_shader_library; use bevy_render::render_graph::{NodeRunError, RenderGraphContext}; use bevy_render::render_phase::ViewSortedRenderPhases; @@ -243,7 +245,7 @@ pub enum ExtractedUiItem { /// Ordering: left, top, right, bottom. border: BorderRect, node_type: NodeType, - transform: Mat4, + transform: Affine2, }, /// A contiguous sequence of text glyphs from the same section Glyphs { @@ -253,7 +255,7 @@ pub enum ExtractedUiItem { } pub struct ExtractedGlyph { - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, } @@ -344,7 +346,7 @@ pub fn extract_uinode_background_colors( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -383,7 +385,7 @@ pub fn extract_uinode_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: uinode.border(), @@ -403,7 +405,7 @@ pub fn extract_uinode_images( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -467,7 +469,7 @@ pub fn extract_uinode_images( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: image.flip_x, flip_y: image.flip_y, border: uinode.border, @@ -487,7 +489,7 @@ pub fn extract_uinode_borders( Entity, &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -503,7 +505,7 @@ pub fn extract_uinode_borders( entity, node, computed_node, - global_transform, + transform, inherited_visibility, maybe_clip, camera, @@ -567,7 +569,7 @@ pub fn extract_uinode_borders( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: global_transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: computed_node.border(), @@ -600,7 +602,7 @@ pub fn extract_uinode_borders( clip: maybe_clip.map(|clip| clip.clip), extracted_camera_entity, item: ExtractedUiItem::Node { - transform: global_transform.compute_matrix(), + transform: transform.into(), atlas_scaling: None, flip_x: false, flip_y: false, @@ -749,7 +751,7 @@ pub fn extract_viewport_nodes( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -792,7 +794,7 @@ pub fn extract_viewport_nodes( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: uinode.border(), @@ -812,7 +814,7 @@ pub fn extract_text_sections( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -830,7 +832,7 @@ pub fn extract_text_sections( for ( entity, uinode, - global_transform, + transform, inherited_visibility, clip, camera, @@ -847,8 +849,7 @@ pub fn extract_text_sections( continue; }; - let transform = global_transform.affine() - * bevy_math::Affine3A::from_translation((-0.5 * uinode.size()).extend(0.)); + let transform = Affine2::from(*transform) * Affine2::from_translation(-0.5 * uinode.size()); for ( i, @@ -866,7 +867,7 @@ pub fn extract_text_sections( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: transform * Affine2::from_translation(*position), rect, }); @@ -910,8 +911,8 @@ pub fn extract_text_shadows( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &ComputedNodeTarget, - &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &TextLayoutInfo, @@ -924,16 +925,8 @@ pub fn extract_text_shadows( let mut end = start + 1; let mut camera_mapper = camera_map.get_mapper(); - for ( - entity, - uinode, - target, - global_transform, - inherited_visibility, - clip, - text_layout_info, - shadow, - ) in &uinode_query + for (entity, uinode, transform, target, inherited_visibility, clip, text_layout_info, shadow) in + &uinode_query { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !inherited_visibility.get() || uinode.is_empty() { @@ -944,9 +937,9 @@ pub fn extract_text_shadows( continue; }; - let transform = global_transform.affine() - * Mat4::from_translation( - (-0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor()).extend(0.), + let node_transform = Affine2::from(*transform) + * Affine2::from_translation( + -0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor(), ); for ( @@ -965,7 +958,7 @@ pub fn extract_text_shadows( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: node_transform * Affine2::from_translation(*position), rect, }); @@ -998,7 +991,7 @@ pub fn extract_text_background_colors( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -1021,8 +1014,8 @@ pub fn extract_text_background_colors( continue; }; - let transform = global_transform.affine() - * bevy_math::Affine3A::from_translation(-0.5 * uinode.size().extend(0.)); + let transform = + Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); for &(section_entity, rect) in text_layout_info.section_rects.iter() { let Ok(text_background_color) = text_background_colors_query.get(section_entity) else { @@ -1042,7 +1035,7 @@ pub fn extract_text_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform * Mat4::from_translation(rect.center().extend(0.)), + transform: transform * Affine2::from_translation(rect.center()), flip_x: false, flip_y: false, border: uinode.border(), @@ -1093,11 +1086,11 @@ impl Default for UiMeta { } } -pub(crate) const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ - Vec3::new(-0.5, -0.5, 0.0), - Vec3::new(0.5, -0.5, 0.0), - Vec3::new(0.5, 0.5, 0.0), - Vec3::new(-0.5, 0.5, 0.0), +pub(crate) const QUAD_VERTEX_POSITIONS: [Vec2; 4] = [ + Vec2::new(-0.5, -0.5), + Vec2::new(0.5, -0.5), + Vec2::new(0.5, 0.5), + Vec2::new(-0.5, 0.5), ]; pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; @@ -1321,12 +1314,12 @@ pub fn prepare_uinodes( let mut uinode_rect = extracted_uinode.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (*transform * (pos * rect_size).extend(1.)).xyz()); - let points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); + .map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); + let points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -1367,7 +1360,7 @@ pub fn prepare_uinodes( points[3] + positions_diff[3], ]; - let transformed_rect_size = transform.transform_vector3(rect_size); + let transformed_rect_size = transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -1448,7 +1441,7 @@ pub fn prepare_uinodes( border_radius.bottom_left, ], border: [border.left, border.top, border.right, border.bottom], - size: rect_size.xy().into(), + size: rect_size.into(), point: points[i].into(), }); } @@ -1470,13 +1463,14 @@ pub fn prepare_uinodes( let color = extracted_uinode.color.to_f32_array(); for glyph in &extracted_uinodes.glyphs[range.clone()] { let glyph_rect = glyph.rect; - let size = glyph.rect.size(); - - let rect_size = glyph_rect.size().extend(1.0); + let rect_size = glyph_rect.size(); // Specify the corners of the glyph let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (glyph.transform * (pos * rect_size).extend(1.)).xyz() + glyph + .transform + .transform_point2(pos * glyph_rect.size()) + .extend(0.) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -1511,7 +1505,7 @@ pub fn prepare_uinodes( // cull nodes that are completely clipped let transformed_rect_size = - glyph.transform.transform_vector3(rect_size); + glyph.transform.transform_vector2(rect_size); if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x.abs() || positions_diff[1].y - positions_diff[2].y @@ -1548,7 +1542,7 @@ pub fn prepare_uinodes( flags: shader_flags::TEXTURED | shader_flags::CORNERS[i], radius: [0.0; 4], border: [0.0; 4], - size: size.into(), + size: rect_size.into(), point: [0.0; 2], }); } diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 02fab4fdee..ebdeacccf9 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -1,9 +1,7 @@ -use core::{hash::Hash, marker::PhantomData, ops::Range}; - use crate::*; use bevy_asset::*; use bevy_ecs::{ - prelude::Component, + prelude::{Component, With}, query::ROQueryItem, system::{ lifetimeless::{Read, SRes}, @@ -11,24 +9,22 @@ use bevy_ecs::{ }, }; use bevy_image::BevyDefault as _; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_render::{ extract_component::ExtractComponentPlugin, globals::{GlobalsBuffer, GlobalsUniform}, + load_shader_library, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_phase::*, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderDevice, RenderQueue}, + sync_world::{MainEntity, TemporaryRenderEntity}, view::*, Extract, ExtractSchedule, Render, RenderSystems, }; -use bevy_render::{ - load_shader_library, - sync_world::{MainEntity, TemporaryRenderEntity}, -}; use bevy_sprite::BorderRect; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; +use core::{hash::Hash, marker::PhantomData, ops::Range}; /// Adds the necessary ECS resources and render logic to enable rendering entities using the given /// [`UiMaterial`] asset type (which includes [`UiMaterial`] types). @@ -321,7 +317,7 @@ impl RenderCommand

for DrawUiMaterialNode { pub struct ExtractedUiMaterialNode { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub border: BorderRect, pub border_radius: ResolvedBorderRadius, @@ -356,7 +352,7 @@ pub fn extract_ui_material_nodes( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &MaterialNode, &InheritedVisibility, Option<&CalculatedClip>, @@ -387,7 +383,7 @@ pub fn extract_ui_material_nodes( extracted_uinodes.uinodes.push(ExtractedUiMaterialNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: computed_node.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), material: handle.id(), rect: Rect { min: Vec2::ZERO, @@ -459,10 +455,13 @@ pub fn prepare_uimaterial_nodes( let uinode_rect = extracted_uinode.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (extracted_uinode.transform * (pos * rect_size).extend(1.0)).xyz() + extracted_uinode + .transform + .transform_point2(pos * rect_size) + .extend(1.0) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -496,7 +495,7 @@ pub fn prepare_uimaterial_nodes( ]; let transformed_rect_size = - extracted_uinode.transform.transform_vector3(rect_size); + extracted_uinode.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 80a55bbcd4..0e232ab1cc 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -1,5 +1,6 @@ use core::{hash::Hash, ops::Range}; +use crate::prelude::UiGlobalTransform; use crate::*; use bevy_asset::*; use bevy_color::{Alpha, ColorToComponents, LinearRgba}; @@ -11,7 +12,7 @@ use bevy_ecs::{ }, }; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_platform::collections::HashMap; use bevy_render::sync_world::MainEntity; use bevy_render::{ @@ -25,7 +26,6 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer}; -use bevy_transform::prelude::GlobalTransform; use binding_types::{sampler, texture_2d}; use bytemuck::{Pod, Zeroable}; use widget::ImageNode; @@ -218,7 +218,7 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline { pub struct ExtractedUiTextureSlice { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub atlas_rect: Option, pub image: AssetId, @@ -246,7 +246,7 @@ pub fn extract_ui_texture_slices( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -306,7 +306,7 @@ pub fn extract_ui_texture_slices( extracted_ui_slicers.slices.push(ExtractedUiTextureSlice { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), color: image.color.into(), rect: Rect { min: Vec2::ZERO, @@ -497,11 +497,12 @@ pub fn prepare_ui_slices( let uinode_rect = texture_slices.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (texture_slices.transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + (texture_slices.transform.transform_point2(pos * rect_size)).extend(0.) + }); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -536,7 +537,7 @@ pub fn prepare_ui_slices( ]; let transformed_rect_size = - texture_slices.transform.transform_vector3(rect_size); + texture_slices.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index f5f914bdc0..343d8af3cd 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1,4 +1,7 @@ -use crate::{FocusPolicy, UiRect, Val}; +use crate::{ + ui_transform::{UiGlobalTransform, UiTransform}, + FocusPolicy, UiRect, Val, +}; use bevy_color::{Alpha, Color}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, system::SystemParam}; @@ -9,7 +12,6 @@ use bevy_render::{ view::Visibility, }; use bevy_sprite::BorderRect; -use bevy_transform::components::Transform; use bevy_utils::once; use bevy_window::{PrimaryWindow, WindowRef}; use core::{f32, num::NonZero}; @@ -229,6 +231,73 @@ impl ComputedNode { pub const fn inverse_scale_factor(&self) -> f32 { self.inverse_scale_factor } + + // Returns true if `point` within the node. + // + // Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. + pub fn contains_point(&self, transform: UiGlobalTransform, point: Vec2) -> bool { + let Some(local_point) = transform + .try_inverse() + .map(|transform| transform.transform_point2(point)) + else { + return false; + }; + let [top, bottom] = if local_point.x < 0. { + [self.border_radius.top_left, self.border_radius.bottom_left] + } else { + [ + self.border_radius.top_right, + self.border_radius.bottom_right, + ] + }; + let r = if local_point.y < 0. { top } else { bottom }; + let corner_to_point = local_point.abs() - 0.5 * self.size; + let q = corner_to_point + r; + let l = q.max(Vec2::ZERO).length(); + let m = q.max_element().min(0.); + l + m - r < 0. + } + + /// Transform a point to normalized node space with the center of the node at the origin and the corners at [+/-0.5, +/-0.5] + pub fn normalize_point(&self, transform: UiGlobalTransform, point: Vec2) -> Option { + self.size + .cmpgt(Vec2::ZERO) + .all() + .then(|| transform.try_inverse()) + .flatten() + .map(|transform| transform.transform_point2(point) / self.size) + } + + /// Resolve the node's clipping rect in local space + pub fn resolve_clip_rect( + &self, + overflow: Overflow, + overflow_clip_margin: OverflowClipMargin, + ) -> Rect { + let mut clip_rect = Rect::from_center_size(Vec2::ZERO, self.size); + + let clip_inset = match overflow_clip_margin.visual_box { + OverflowClipBox::BorderBox => BorderRect::ZERO, + OverflowClipBox::ContentBox => self.content_inset(), + OverflowClipBox::PaddingBox => self.border(), + }; + + clip_rect.min.x += clip_inset.left; + clip_rect.min.y += clip_inset.top; + clip_rect.max.x -= clip_inset.right; + clip_rect.max.y -= clip_inset.bottom; + + if overflow.x == OverflowAxis::Visible { + clip_rect.min.x = -f32::INFINITY; + clip_rect.max.x = f32::INFINITY; + } + if overflow.y == OverflowAxis::Visible { + clip_rect.min.y = -f32::INFINITY; + clip_rect.max.y = f32::INFINITY; + } + + clip_rect + } } impl ComputedNode { @@ -323,12 +392,12 @@ impl From for ScrollPosition { #[require( ComputedNode, ComputedNodeTarget, + UiTransform, BackgroundColor, BorderColor, BorderRadius, FocusPolicy, ScrollPosition, - Transform, Visibility, ZIndex )] diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs new file mode 100644 index 0000000000..47f8484e54 --- /dev/null +++ b/crates/bevy_ui/src/ui_transform.rs @@ -0,0 +1,191 @@ +use crate::Val; +use bevy_derive::Deref; +use bevy_ecs::component::Component; +use bevy_ecs::prelude::ReflectComponent; +use bevy_math::Affine2; +use bevy_math::Rot2; +use bevy_math::Vec2; +use bevy_reflect::prelude::*; + +/// A pair of [`Val`]s used to represent a 2-dimensional size or offset. +#[derive(Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct Val2 { + /// Translate the node along the x-axis. + /// `Val::Percent` values are resolved based on the computed width of the Ui Node. + /// `Val::Auto` is resolved to `0.`. + pub x: Val, + /// Translate the node along the y-axis. + /// `Val::Percent` values are resolved based on the computed height of the UI Node. + /// `Val::Auto` is resolved to `0.`. + pub y: Val, +} + +impl Val2 { + pub const ZERO: Self = Self { + x: Val::ZERO, + y: Val::ZERO, + }; + + /// Creates a new [`Val2`] where both components are in logical pixels + pub const fn px(x: f32, y: f32) -> Self { + Self { + x: Val::Px(x), + y: Val::Px(y), + } + } + + /// Creates a new [`Val2`] where both components are percentage values + pub const fn percent(x: f32, y: f32) -> Self { + Self { + x: Val::Percent(x), + y: Val::Percent(y), + } + } + + /// Creates a new [`Val2`] + pub const fn new(x: Val, y: Val) -> Self { + Self { x, y } + } + + /// Resolves this [`Val2`] from the given `scale_factor`, `parent_size`, + /// and `viewport_size`. + /// + /// Component values of [`Val::Auto`] are resolved to 0. + pub fn resolve(&self, scale_factor: f32, base_size: Vec2, viewport_size: Vec2) -> Vec2 { + Vec2::new( + self.x + .resolve(scale_factor, base_size.x, viewport_size) + .unwrap_or(0.), + self.y + .resolve(scale_factor, base_size.y, viewport_size) + .unwrap_or(0.), + ) + } +} + +impl Default for Val2 { + fn default() -> Self { + Self::ZERO + } +} + +/// Relative 2D transform for UI nodes +/// +/// [`UiGlobalTransform`] is automatically inserted whenever [`UiTransform`] is inserted. +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[require(UiGlobalTransform)] +pub struct UiTransform { + /// Translate the node. + pub translation: Val2, + /// Scale the node. A negative value reflects the node in that axis. + pub scale: Vec2, + /// Rotate the node clockwise. + pub rotation: Rot2, +} + +impl UiTransform { + pub const IDENTITY: Self = Self { + translation: Val2::ZERO, + scale: Vec2::ONE, + rotation: Rot2::IDENTITY, + }; + + /// Creates a UI transform representing a rotation. + pub fn from_rotation(rotation: Rot2) -> Self { + Self { + rotation, + ..Self::IDENTITY + } + } + + /// Creates a UI transform representing a responsive translation. + pub fn from_translation(translation: Val2) -> Self { + Self { + translation, + ..Self::IDENTITY + } + } + + /// Creates a UI transform representing a scaling. + pub fn from_scale(scale: Vec2) -> Self { + Self { + scale, + ..Self::IDENTITY + } + } + + /// Resolves the translation from the given `scale_factor`, `base_value`, and `target_size` + /// and returns a 2d affine transform from the resolved translation, and the `UiTransform`'s rotation, and scale. + pub fn compute_affine(&self, scale_factor: f32, base_size: Vec2, target_size: Vec2) -> Affine2 { + Affine2::from_scale_angle_translation( + self.scale, + self.rotation.as_radians(), + self.translation + .resolve(scale_factor, base_size, target_size), + ) + } +} + +impl Default for UiTransform { + fn default() -> Self { + Self::IDENTITY + } +} + +/// Absolute 2D transform for UI nodes +/// +/// [`UiGlobalTransform`]s are updated from [`UiTransform`] and [`Node`](crate::ui_node::Node) +/// in [`ui_layout_system`](crate::layout::ui_layout_system) +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect, Deref)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct UiGlobalTransform(Affine2); + +impl Default for UiGlobalTransform { + fn default() -> Self { + Self(Affine2::IDENTITY) + } +} + +impl UiGlobalTransform { + /// If the transform is invertible returns its inverse. + /// Otherwise returns `None`. + #[inline] + pub fn try_inverse(&self) -> Option { + (self.matrix2.determinant() != 0.).then_some(self.inverse()) + } +} + +impl From for UiGlobalTransform { + fn from(value: Affine2) -> Self { + Self(value) + } +} + +impl From for Affine2 { + fn from(value: UiGlobalTransform) -> Self { + value.0 + } +} + +impl From<&UiGlobalTransform> for Affine2 { + fn from(value: &UiGlobalTransform) -> Self { + value.0 + } +} diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 7e27c4abdd..c0e9d09d7b 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -2,6 +2,7 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, + ui_transform::UiGlobalTransform, CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale, UiTargetCamera, }; @@ -17,7 +18,6 @@ use bevy_ecs::{ use bevy_math::{Rect, UVec2}; use bevy_render::camera::Camera; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; /// Updates clipping for all nodes pub fn update_clipping_system( @@ -26,7 +26,7 @@ pub fn update_clipping_system( mut node_query: Query<( &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, Option<&mut CalculatedClip>, )>, ui_children: UiChildren, @@ -48,14 +48,13 @@ fn update_clipping( node_query: &mut Query<( &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, Option<&mut CalculatedClip>, )>, entity: Entity, mut maybe_inherited_clip: Option, ) { - let Ok((node, computed_node, global_transform, maybe_calculated_clip)) = - node_query.get_mut(entity) + let Ok((node, computed_node, transform, maybe_calculated_clip)) = node_query.get_mut(entity) else { return; }; @@ -91,10 +90,7 @@ fn update_clipping( maybe_inherited_clip } else { // Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists - let mut clip_rect = Rect::from_center_size( - global_transform.translation().truncate(), - computed_node.size(), - ); + let mut clip_rect = Rect::from_center_size(transform.translation, computed_node.size()); // Content isn't clipped at the edges of the node but at the edges of the region specified by [`Node::overflow_clip_margin`]. // diff --git a/crates/bevy_ui/src/widget/viewport.rs b/crates/bevy_ui/src/widget/viewport.rs index f68033ea7f..9cdc348da5 100644 --- a/crates/bevy_ui/src/widget/viewport.rs +++ b/crates/bevy_ui/src/widget/viewport.rs @@ -171,11 +171,6 @@ pub fn update_viewport_render_target_size( height: u32::max(1, size.y as u32), ..default() }; - let image = images.get_mut(image_handle).unwrap(); - if image.data.is_some() { - image.resize(size); - } else { - image.texture_descriptor.size = size; - } + images.get_mut(image_handle).unwrap().resize(size); } } diff --git a/crates/bevy_winit/src/cursor.rs b/crates/bevy_winit/src/cursor.rs index bdca3f8585..63c8a2bb2d 100644 --- a/crates/bevy_winit/src/cursor.rs +++ b/crates/bevy_winit/src/cursor.rs @@ -194,7 +194,7 @@ fn update_cursors( fn on_remove_cursor_icon(trigger: Trigger, mut commands: Commands) { // Use `try_insert` to avoid panic if the window is being destroyed. commands - .entity(trigger.target()) + .entity(trigger.target().unwrap()) .try_insert(PendingCursor(Some(CursorSource::System( convert_system_cursor_icon(SystemCursorIcon::Default), )))); diff --git a/examples/3d/edit_material_on_gltf.rs b/examples/3d/edit_material_on_gltf.rs index f9de5842a9..029ec6bf1e 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()) else { + let Ok(color_override) = color_override.get(trigger.target().unwrap()) else { return; }; // Iterate over all children recursively - for descendants in children.iter_descendants(trigger.target()) { + for descendants in children.iter_descendants(trigger.target().unwrap()) { // Get the material of the descendant if let Some(material) = mesh_materials .get(descendants) diff --git a/examples/README.md b/examples/README.md index 37c1ae4621..dce7c114e8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -571,6 +571,7 @@ Example | Description [UI Texture Atlas Slice](../examples/ui/ui_texture_atlas_slice.rs) | Illustrates how to use 9 Slicing for TextureAtlases in UI [UI Texture Slice](../examples/ui/ui_texture_slice.rs) | Illustrates how to use 9 Slicing in UI [UI Texture Slice Flipping and Tiling](../examples/ui/ui_texture_slice_flip_and_tile.rs) | Illustrates how to flip and tile images with 9 Slicing in UI +[UI Transform](../examples/ui/ui_transform.rs) | An example demonstrating how to translate, rotate and scale UI elements. [UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements [Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates [Viewport Node](../examples/ui/viewport_node.rs) | Demonstrates how to create a viewport node with picking support diff --git a/examples/animation/animated_mesh.rs b/examples/animation/animated_mesh.rs index ecea86fb17..d2c4fd8443 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()) { + if let Ok(animation_to_play) = animations_to_play.get(trigger.target().unwrap()) { // 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()) { + for child in children.iter_descendants(trigger.target().unwrap()) { 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 2048f573fd..c0f261752e 100644 --- a/examples/animation/animated_mesh_events.rs +++ b/examples/animation/animated_mesh_events.rs @@ -47,7 +47,10 @@ fn observe_on_step( transforms: Query<&GlobalTransform>, mut seeded_rng: ResMut, ) { - let translation = transforms.get(trigger.target()).unwrap().translation(); + let translation = transforms + .get(trigger.target().unwrap()) + .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/ecs/entity_disabling.rs b/examples/ecs/entity_disabling.rs index 86cf8e9d6c..f9cebede22 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(); + let clicked_entity = trigger.target().unwrap(); // 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/observer_propagation.rs b/examples/ecs/observer_propagation.rs index 1acf5efa90..6793364a29 100644 --- a/examples/ecs/observer_propagation.rs +++ b/examples/ecs/observer_propagation.rs @@ -78,14 +78,14 @@ fn attack_armor(entities: Query>, mut commands: Commands) { } fn attack_hits(trigger: Trigger, name: Query<&Name>) { - if let Ok(name) = name.get(trigger.target()) { + 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)>) { - let (armor, name) = armor.get(trigger.target()).unwrap(); + let (armor, name) = armor.get(trigger.target().unwrap()).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(); + let (mut hp, name) = hp.get_mut(trigger.target().unwrap()).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()).despawn(); + commands.entity(trigger.target().unwrap()).despawn(); app_exit.write(AppExit::Success); } diff --git a/examples/ecs/observers.rs b/examples/ecs/observers.rs index 41a2e5dd07..d7bf067d4c 100644 --- a/examples/ecs/observers.rs +++ b/examples/ecs/observers.rs @@ -117,12 +117,16 @@ fn on_add_mine( query: Query<&Mine>, mut index: ResMut, ) { - let mine = query.get(trigger.target()).unwrap(); + let mine = query.get(trigger.target().unwrap()).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()); + index + .map + .entry(tile) + .or_default() + .insert(trigger.target().unwrap()); } // Remove despawned mines from our index @@ -131,19 +135,19 @@ fn on_remove_mine( query: Query<&Mine>, mut index: ResMut, ) { - let mine = query.get(trigger.target()).unwrap(); + let mine = query.get(trigger.target().unwrap()).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()); + set.remove(&trigger.target().unwrap()); }); } fn explode_mine(trigger: Trigger, 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(); + let id = trigger.target().unwrap(); 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 782576aae0..f60452f4ea 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: Trigger, mut query: Query<&mut Sprite>) { // The `OnRemove` trigger was automatically called on the `Entity` that had its `MyComponent` removed. - let entity = trigger.target(); + 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 c8aa35d799..ec767b763d 100644 --- a/examples/no_std/library/src/lib.rs +++ b/examples/no_std/library/src/lib.rs @@ -127,7 +127,10 @@ fn tick_timers( } fn unwrap(trigger: Trigger, world: &mut World) { - if let Ok(mut target) = world.get_entity_mut(trigger.target()) { + if let Some(mut target) = trigger + .target() + .and_then(|target| world.get_entity_mut(target).ok()) + { 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 4213775ba7..5461d9e4c9 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: Trigger>, mut texts: Query<&mut TextColor>| { - let mut text_color = texts.get_mut(out.target()).unwrap(); + let mut text_color = texts.get_mut(out.target().unwrap()).unwrap(); text_color.0 = Color::WHITE; }, ) .observe( |over: Trigger>, mut texts: Query<&mut TextColor>| { - let mut color = texts.get_mut(over.target()).unwrap(); + let mut color = texts.get_mut(over.target().unwrap()).unwrap(); color.0 = bevy::color::palettes::tailwind::CYAN_400.into(); }, ); @@ -102,7 +102,7 @@ fn on_click_spawn_cube( } fn on_drag_rotate(drag: Trigger>, mut transforms: Query<&mut Transform>) { - if let Ok(mut transform) = transforms.get_mut(drag.target()) { + 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 3c0d3bf09f..4c247aa62a 100644 --- a/examples/picking/mesh_picking.rs +++ b/examples/picking/mesh_picking.rs @@ -164,7 +164,7 @@ fn update_material_on( // 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()) { + if let Ok(mut material) = query.get_mut(trigger.target().unwrap()) { material.0 = new_material.clone(); } } @@ -191,7 +191,7 @@ fn rotate(mut query: Query<&mut Transform, With>, time: Res