Merge branch 'bevyengine:main' into schema-types-metadata

This commit is contained in:
MevLyshkin 2025-06-09 22:06:22 +02:00 committed by GitHub
commit 7beca6fd0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
79 changed files with 1342 additions and 666 deletions

View File

@ -3543,6 +3543,17 @@ description = "Illustrates how to use 9 Slicing for TextureAtlases in UI"
category = "UI (User Interface)" category = "UI (User Interface)"
wasm = true 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]] [[example]]
name = "viewport_debug" name = "viewport_debug"
path = "examples/ui/viewport_debug.rs" path = "examples/ui/viewport_debug.rs"

View File

@ -340,8 +340,8 @@ let mut world = World::new();
let entity = world.spawn_empty().id(); let entity = world.spawn_empty().id();
world.add_observer(|trigger: Trigger<Explode>, mut commands: Commands| { world.add_observer(|trigger: Trigger<Explode>, mut commands: Commands| {
println!("Entity {} goes BOOM!", trigger.target()); println!("Entity {} goes BOOM!", trigger.target().unwrap());
commands.entity(trigger.target()).despawn(); commands.entity(trigger.target().unwrap()).despawn();
}); });
world.flush(); world.flush();

View File

@ -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))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
extern crate proc_macro; extern crate proc_macro;
@ -29,6 +30,7 @@ enum BundleFieldKind {
const BUNDLE_ATTRIBUTE_NAME: &str = "bundle"; const BUNDLE_ATTRIBUTE_NAME: &str = "bundle";
const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore"; const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore";
/// Implement the `Bundle` trait.
#[proc_macro_derive(Bundle, attributes(bundle))] #[proc_macro_derive(Bundle, attributes(bundle))]
pub fn derive_bundle(input: TokenStream) -> TokenStream { pub fn derive_bundle(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput); 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))] #[proc_macro_derive(MapEntities, attributes(entities))]
pub fn derive_map_entities(input: TokenStream) -> TokenStream { pub fn derive_map_entities(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput); 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") BevyManifest::shared().get_path("bevy_ecs")
} }
/// Implement the `Event` trait.
#[proc_macro_derive(Event, attributes(event))] #[proc_macro_derive(Event, attributes(event))]
pub fn derive_event(input: TokenStream) -> TokenStream { pub fn derive_event(input: TokenStream) -> TokenStream {
component::derive_event(input) component::derive_event(input)
} }
/// Implement the `Resource` trait.
#[proc_macro_derive(Resource)] #[proc_macro_derive(Resource)]
pub fn derive_resource(input: TokenStream) -> TokenStream { pub fn derive_resource(input: TokenStream) -> TokenStream {
component::derive_resource(input) component::derive_resource(input)
} }
/// Implement the `Component` trait.
#[proc_macro_derive( #[proc_macro_derive(
Component, Component,
attributes(component, require, relationship, relationship_target, entities) attributes(component, require, relationship, relationship_target, entities)
@ -540,6 +546,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
component::derive_component(input) component::derive_component(input)
} }
/// Implement the `FromWorld` trait.
#[proc_macro_derive(FromWorld, attributes(from_world))] #[proc_macro_derive(FromWorld, attributes(from_world))]
pub fn derive_from_world(input: TokenStream) -> TokenStream { pub fn derive_from_world(input: TokenStream) -> TokenStream {
let bevy_ecs_path = bevy_ecs_path(); let bevy_ecs_path = bevy_ecs_path();

View File

@ -1143,7 +1143,7 @@ impl<'w> BundleInserter<'w> {
if archetype.has_replace_observer() { if archetype.has_replace_observer() {
deferred_world.trigger_observers( deferred_world.trigger_observers(
ON_REPLACE, ON_REPLACE,
entity, Some(entity),
archetype_after_insert.iter_existing(), archetype_after_insert.iter_existing(),
caller, caller,
); );
@ -1328,7 +1328,7 @@ impl<'w> BundleInserter<'w> {
if new_archetype.has_add_observer() { if new_archetype.has_add_observer() {
deferred_world.trigger_observers( deferred_world.trigger_observers(
ON_ADD, ON_ADD,
entity, Some(entity),
archetype_after_insert.iter_added(), archetype_after_insert.iter_added(),
caller, caller,
); );
@ -1346,7 +1346,7 @@ impl<'w> BundleInserter<'w> {
if new_archetype.has_insert_observer() { if new_archetype.has_insert_observer() {
deferred_world.trigger_observers( deferred_world.trigger_observers(
ON_INSERT, ON_INSERT,
entity, Some(entity),
archetype_after_insert.iter_inserted(), archetype_after_insert.iter_inserted(),
caller, caller,
); );
@ -1365,7 +1365,7 @@ impl<'w> BundleInserter<'w> {
if new_archetype.has_insert_observer() { if new_archetype.has_insert_observer() {
deferred_world.trigger_observers( deferred_world.trigger_observers(
ON_INSERT, ON_INSERT,
entity, Some(entity),
archetype_after_insert.iter_added(), archetype_after_insert.iter_added(),
caller, caller,
); );
@ -1519,7 +1519,7 @@ impl<'w> BundleRemover<'w> {
if self.old_archetype.as_ref().has_replace_observer() { if self.old_archetype.as_ref().has_replace_observer() {
deferred_world.trigger_observers( deferred_world.trigger_observers(
ON_REPLACE, ON_REPLACE,
entity, Some(entity),
bundle_components_in_archetype(), bundle_components_in_archetype(),
caller, caller,
); );
@ -1534,7 +1534,7 @@ impl<'w> BundleRemover<'w> {
if self.old_archetype.as_ref().has_remove_observer() { if self.old_archetype.as_ref().has_remove_observer() {
deferred_world.trigger_observers( deferred_world.trigger_observers(
ON_REMOVE, ON_REMOVE,
entity, Some(entity),
bundle_components_in_archetype(), bundle_components_in_archetype(),
caller, caller,
); );
@ -1785,7 +1785,7 @@ impl<'w> BundleSpawner<'w> {
if archetype.has_add_observer() { if archetype.has_add_observer() {
deferred_world.trigger_observers( deferred_world.trigger_observers(
ON_ADD, ON_ADD,
entity, Some(entity),
bundle_info.iter_contributed_components(), bundle_info.iter_contributed_components(),
caller, caller,
); );
@ -1800,7 +1800,7 @@ impl<'w> BundleSpawner<'w> {
if archetype.has_insert_observer() { if archetype.has_insert_observer() {
deferred_world.trigger_observers( deferred_world.trigger_observers(
ON_INSERT, ON_INSERT,
entity, Some(entity),
bundle_info.iter_contributed_components(), bundle_info.iter_contributed_components(),
caller, caller,
); );

View File

@ -898,63 +898,39 @@ impl_debug!(Ref<'w, T>,);
/// Unique mutable borrow of an entity's component or of a resource. /// 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 /// This can be used in queries to access change detection from immutable query methods, as opposed
/// `&mut T`, which only provides access to change detection while in its mutable form: /// to `&mut T` which only provides access to change detection from mutable query methods.
/// ///
/// ```rust /// ```rust
/// # use bevy_ecs::prelude::*; /// # use bevy_ecs::prelude::*;
/// # use bevy_ecs::query::QueryData; /// # use bevy_ecs::query::QueryData;
/// # /// #
/// #[derive(Component, Clone)] /// #[derive(Component, Clone, Debug)]
/// struct Name(String); /// struct Name(String);
/// ///
/// #[derive(Component, Clone, Copy)] /// #[derive(Component, Clone, Copy, Debug)]
/// struct Health(f32); /// struct Health(f32);
/// ///
/// #[derive(Component, Clone, Copy)] /// fn my_system(mut query: Query<(Mut<Name>, &mut Health)>) {
/// struct Position { /// // Mutable access provides change detection information for both parameters:
/// x: f32, /// // - `name` has type `Mut<Name>`
/// y: f32, /// // - `health` has type `Mut<Health>`
/// }; /// for (name, health) in query.iter_mut() {
/// /// println!("Name: {:?} (last changed {:?})", name, name.last_changed());
/// #[derive(Component, Clone, Copy)] /// println!("Health: {:?} (last changed: {:?})", health, health.last_changed());
/// struct Player { /// # println!("{}{}", name.0, health.0); // Silence dead_code warning
/// 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<PlayerQuery>) { /// // Immutable access only provides change detection for `Name`:
/// // The item returned by the iterator is of type `PlayerQueryReadOnlyItem`. /// // - `name` has type `Ref<Name>`
/// for player in players_query.iter() { /// // - `health` has type `&Health`
/// if player.name.is_changed() { /// for (name, health) in query.iter() {
/// // Update the player's name. This clones a String, and so is more expensive. /// println!("Name: {:?} (last changed {:?})", name, name.last_changed());
/// update_player_name(player.id, player.name.clone()); /// println!("Health: {:?}", health);
/// }
///
/// // Update the health bar.
/// update_player_health(player.id, *player.health);
///
/// // Update the player's position.
/// update_player_position(player.id, *player.position);
/// } /// }
/// } /// }
/// ///
/// # bevy_ecs::system::assert_is_system(update_player_avatars); /// # bevy_ecs::system::assert_is_system(my_system);
///
/// # 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) {}
/// ``` /// ```
pub struct Mut<'w, T: ?Sized> { pub struct Mut<'w, T: ?Sized> {
pub(crate) value: &'w mut T, pub(crate) value: &'w mut T,

View File

@ -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 /// 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 /// 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 /// 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 /// This is an important distinction: the entity reacting to an event is not always the same as
/// the entity triggered by the event. /// the entity triggered by the event.
pub fn target(&self) -> Entity { pub fn target(&self) -> Option<Entity> {
self.trigger.target self.trigger.target
} }
@ -341,7 +341,7 @@ pub struct ObserverTrigger {
/// The [`ComponentId`]s the trigger targeted. /// The [`ComponentId`]s the trigger targeted.
components: SmallVec<[ComponentId; 2]>, components: SmallVec<[ComponentId; 2]>,
/// The entity the trigger targeted. /// The entity the trigger targeted.
pub target: Entity, pub target: Option<Entity>,
/// The location of the source code that triggered the observer. /// The location of the source code that triggered the observer.
pub caller: MaybeLocation, pub caller: MaybeLocation,
} }
@ -416,7 +416,7 @@ impl Observers {
pub(crate) fn invoke<T>( pub(crate) fn invoke<T>(
mut world: DeferredWorld, mut world: DeferredWorld,
event_type: ComponentId, event_type: ComponentId,
target: Entity, target: Option<Entity>,
components: impl Iterator<Item = ComponentId> + Clone, components: impl Iterator<Item = ComponentId> + Clone,
data: &mut T, data: &mut T,
propagate: &mut bool, propagate: &mut bool,
@ -455,8 +455,8 @@ impl Observers {
observers.map.iter().for_each(&mut trigger_observer); observers.map.iter().for_each(&mut trigger_observer);
// Trigger entity observers listening for this kind of trigger // Trigger entity observers listening for this kind of trigger
if target != Entity::PLACEHOLDER { if let Some(target_entity) = target {
if let Some(map) = observers.entity_observers.get(&target) { if let Some(map) = observers.entity_observers.get(&target_entity) {
map.iter().for_each(&mut trigger_observer); map.iter().for_each(&mut trigger_observer);
} }
} }
@ -469,8 +469,8 @@ impl Observers {
.iter() .iter()
.for_each(&mut trigger_observer); .for_each(&mut trigger_observer);
if target != Entity::PLACEHOLDER { if let Some(target_entity) = target {
if let Some(map) = component_observers.entity_map.get(&target) { if let Some(map) = component_observers.entity_map.get(&target_entity) {
map.iter().for_each(&mut trigger_observer); map.iter().for_each(&mut trigger_observer);
} }
} }
@ -695,7 +695,7 @@ impl World {
unsafe { unsafe {
world.trigger_observers_with_data::<_, E::Traversal>( world.trigger_observers_with_data::<_, E::Traversal>(
event_id, event_id,
Entity::PLACEHOLDER, None,
targets.components(), targets.components(),
event_data, event_data,
false, false,
@ -708,7 +708,7 @@ impl World {
unsafe { unsafe {
world.trigger_observers_with_data::<_, E::Traversal>( world.trigger_observers_with_data::<_, E::Traversal>(
event_id, event_id,
target_entity, Some(target_entity),
targets.components(), targets.components(),
event_data, event_data,
E::AUTO_PROPAGATE, E::AUTO_PROPAGATE,
@ -999,20 +999,20 @@ mod tests {
world.add_observer( world.add_observer(
|obs: Trigger<OnAdd, A>, mut res: ResMut<Order>, mut commands: Commands| { |obs: Trigger<OnAdd, A>, mut res: ResMut<Order>, mut commands: Commands| {
res.observed("add_a"); res.observed("add_a");
commands.entity(obs.target()).insert(B); commands.entity(obs.target().unwrap()).insert(B);
}, },
); );
world.add_observer( world.add_observer(
|obs: Trigger<OnRemove, A>, mut res: ResMut<Order>, mut commands: Commands| { |obs: Trigger<OnRemove, A>, mut res: ResMut<Order>, mut commands: Commands| {
res.observed("remove_a"); res.observed("remove_a");
commands.entity(obs.target()).remove::<B>(); commands.entity(obs.target().unwrap()).remove::<B>();
}, },
); );
world.add_observer( world.add_observer(
|obs: Trigger<OnAdd, B>, mut res: ResMut<Order>, mut commands: Commands| { |obs: Trigger<OnAdd, B>, mut res: ResMut<Order>, mut commands: Commands| {
res.observed("add_b"); res.observed("add_b");
commands.entity(obs.target()).remove::<A>(); commands.entity(obs.target().unwrap()).remove::<A>();
}, },
); );
world.add_observer(|_: Trigger<OnRemove, B>, mut res: ResMut<Order>| { world.add_observer(|_: Trigger<OnRemove, B>, mut res: ResMut<Order>| {
@ -1181,7 +1181,7 @@ mod tests {
}; };
world.spawn_empty().observe(system); world.spawn_empty().observe(system);
world.add_observer(move |obs: Trigger<EventA>, mut res: ResMut<Order>| { world.add_observer(move |obs: Trigger<EventA>, mut res: ResMut<Order>| {
assert_eq!(obs.target(), Entity::PLACEHOLDER); assert_eq!(obs.target(), None);
res.observed("event_a"); res.observed("event_a");
}); });
@ -1208,7 +1208,7 @@ mod tests {
.observe(|_: Trigger<EventA>, mut res: ResMut<Order>| res.observed("a_1")) .observe(|_: Trigger<EventA>, mut res: ResMut<Order>| res.observed("a_1"))
.id(); .id();
world.add_observer(move |obs: Trigger<EventA>, mut res: ResMut<Order>| { world.add_observer(move |obs: Trigger<EventA>, mut res: ResMut<Order>| {
assert_eq!(obs.target(), entity); assert_eq!(obs.target().unwrap(), entity);
res.observed("a_2"); res.observed("a_2");
}); });
@ -1628,7 +1628,7 @@ mod tests {
world.add_observer( world.add_observer(
|trigger: Trigger<EventPropagating>, query: Query<&A>, mut res: ResMut<Order>| { |trigger: Trigger<EventPropagating>, query: Query<&A>, mut res: ResMut<Order>| {
if query.get(trigger.target()).is_ok() { if query.get(trigger.target().unwrap()).is_ok() {
res.observed("event"); res.observed("event");
} }
}, },
@ -1651,7 +1651,7 @@ mod tests {
fn observer_modifies_relationship() { fn observer_modifies_relationship() {
fn on_add(trigger: Trigger<OnAdd, A>, mut commands: Commands) { fn on_add(trigger: Trigger<OnAdd, A>, mut commands: Commands) {
commands commands
.entity(trigger.target()) .entity(trigger.target().unwrap())
.with_related_entities::<crate::hierarchy::ChildOf>(|rsc| { .with_related_entities::<crate::hierarchy::ChildOf>(|rsc| {
rsc.spawn_empty(); rsc.spawn_empty();
}); });

View File

@ -123,8 +123,8 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate:
/// struct Explode; /// struct Explode;
/// ///
/// world.add_observer(|trigger: Trigger<Explode>, mut commands: Commands| { /// world.add_observer(|trigger: Trigger<Explode>, mut commands: Commands| {
/// println!("Entity {} goes BOOM!", trigger.target()); /// println!("Entity {} goes BOOM!", trigger.target().unwrap());
/// commands.entity(trigger.target()).despawn(); /// commands.entity(trigger.target().unwrap()).despawn();
/// }); /// });
/// ///
/// world.flush(); /// world.flush();
@ -157,7 +157,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate:
/// # struct Explode; /// # struct Explode;
/// world.entity_mut(e1).observe(|trigger: Trigger<Explode>, mut commands: Commands| { /// world.entity_mut(e1).observe(|trigger: Trigger<Explode>, mut commands: Commands| {
/// println!("Boom!"); /// println!("Boom!");
/// commands.entity(trigger.target()).despawn(); /// commands.entity(trigger.target().unwrap()).despawn();
/// }); /// });
/// ///
/// world.entity_mut(e2).observe(|trigger: Trigger<Explode>, mut commands: Commands| { /// world.entity_mut(e2).observe(|trigger: Trigger<Explode>, mut commands: Commands| {

View File

@ -47,6 +47,8 @@ use variadics_please::all_tuples;
/// - **[`Ref`].** /// - **[`Ref`].**
/// Similar to change detection filters but it is used as a query fetch parameter. /// 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. /// It exposes methods to check for changes to the wrapped component.
/// - **[`Mut`].**
/// Mutable component access, with change detection data.
/// - **[`Has`].** /// - **[`Has`].**
/// Returns a bool indicating whether the entity has the specified component. /// Returns a bool indicating whether the entity has the specified component.
/// ///

View File

@ -1,6 +1,7 @@
use crate::{ use crate::{
bundle::Bundle, bundle::Bundle,
entity::{hash_set::EntityHashSet, Entity}, entity::{hash_set::EntityHashSet, Entity},
prelude::Children,
relationship::{ relationship::{
Relationship, RelationshipHookMode, RelationshipSourceCollection, RelationshipTarget, Relationship, RelationshipHookMode, RelationshipSourceCollection, RelationshipTarget,
}, },
@ -302,6 +303,15 @@ impl<'w> EntityWorldMut<'w> {
self 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::<Children>();
self
}
/// Inserts a component or bundle of components into the entity and all related entities, /// 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. /// 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::<Children>()
}
/// Inserts a component or bundle of components into the entity and all related entities, /// 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. /// traversing the relationship tracked in `S` in a breadth-first manner.
/// ///

View File

@ -20,7 +20,7 @@ use crate::{
prelude::{IntoSystemSet, SystemSet}, prelude::{IntoSystemSet, SystemSet},
query::{Access, FilteredAccessSet}, query::{Access, FilteredAccessSet},
schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet}, schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet},
system::{ScheduleSystem, System, SystemIn, SystemParamValidationError}, system::{ScheduleSystem, System, SystemIn, SystemParamValidationError, SystemStateFlags},
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World},
}; };
@ -171,26 +171,9 @@ impl System for ApplyDeferred {
const { &FilteredAccessSet::new() } const { &FilteredAccessSet::new() }
} }
fn is_send(&self) -> bool { fn flags(&self) -> SystemStateFlags {
// Although this system itself does nothing on its own, the system // non-send , exclusive , no deferred
// executor uses it to apply deferred commands. Commands must be allowed SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE
// 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
} }
unsafe fn run_unsafe( unsafe fn run_unsafe(

View File

@ -874,7 +874,6 @@ mod tests {
} }
#[test] #[test]
#[ignore = "Known failing but fix is non-trivial: https://github.com/bevyengine/bevy/issues/4381"]
fn filtered_components() { fn filtered_components() {
let mut world = World::new(); let mut world = World::new();
world.spawn(A); world.spawn(A);

View File

@ -1418,9 +1418,8 @@ impl ScheduleGraph {
if system_a.is_exclusive() || system_b.is_exclusive() { if system_a.is_exclusive() || system_b.is_exclusive() {
conflicting_systems.push((a, b, Vec::new())); conflicting_systems.push((a, b, Vec::new()));
} else { } else {
let access_a = system_a.component_access(); let access_a = system_a.component_access_set();
let access_b = system_b.component_access(); let access_b = system_b.component_access_set();
if !access_a.is_compatible(access_b) {
match access_a.get_conflicts(access_b) { match access_a.get_conflicts(access_b) {
AccessConflicts::Individual(conflicts) => { AccessConflicts::Individual(conflicts) => {
let conflicts: Vec<_> = conflicts let conflicts: Vec<_> = conflicts
@ -1440,7 +1439,6 @@ impl ScheduleGraph {
} }
} }
} }
}
conflicting_systems conflicting_systems
} }

View File

@ -137,16 +137,9 @@ where
self.system.component_access_set() self.system.component_access_set()
} }
fn is_send(&self) -> bool { #[inline]
self.system.is_send() fn flags(&self) -> super::SystemStateFlags {
} self.system.flags()
fn is_exclusive(&self) -> bool {
self.system.is_exclusive()
}
fn has_deferred(&self) -> bool {
self.system.has_deferred()
} }
#[inline] #[inline]

View File

@ -152,16 +152,9 @@ where
&self.component_access_set &self.component_access_set
} }
fn is_send(&self) -> bool { #[inline]
self.a.is_send() && self.b.is_send() fn flags(&self) -> super::SystemStateFlags {
} self.a.flags() | self.b.flags()
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()
} }
unsafe fn run_unsafe( unsafe fn run_unsafe(
@ -378,16 +371,9 @@ where
&self.component_access_set &self.component_access_set
} }
fn is_send(&self) -> bool { #[inline]
self.a.is_send() && self.b.is_send() fn flags(&self) -> super::SystemStateFlags {
} self.a.flags() | self.b.flags()
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()
} }
unsafe fn run_unsafe( unsafe fn run_unsafe(

View File

@ -13,7 +13,7 @@ use alloc::{borrow::Cow, vec, vec::Vec};
use core::marker::PhantomData; use core::marker::PhantomData;
use variadics_please::all_tuples; use variadics_please::all_tuples;
use super::SystemParamValidationError; use super::{SystemParamValidationError, SystemStateFlags};
/// A function system that runs with exclusive [`World`] access. /// A function system that runs with exclusive [`World`] access.
/// ///
@ -98,22 +98,12 @@ where
} }
#[inline] #[inline]
fn is_send(&self) -> bool { fn flags(&self) -> SystemStateFlags {
// exclusive systems should have access to non-send resources // non-send , exclusive , no deferred
// the executor runs exclusive systems on the main thread, so this // the executor runs exclusive systems on the main thread, so this
// field reflects that constraint // 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 // exclusive systems have no deferred system params
false SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE
} }
#[inline] #[inline]

View File

@ -17,7 +17,9 @@ use variadics_please::all_tuples;
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
use tracing::{info_span, Span}; use tracing::{info_span, Span};
use super::{IntoSystem, ReadOnlySystem, SystemParamBuilder, SystemParamValidationError}; use super::{
IntoSystem, ReadOnlySystem, SystemParamBuilder, SystemParamValidationError, SystemStateFlags,
};
/// The metadata of a [`System`]. /// The metadata of a [`System`].
#[derive(Clone)] #[derive(Clone)]
@ -29,8 +31,7 @@ pub struct SystemMeta {
pub(crate) component_access_set: FilteredAccessSet<ComponentId>, pub(crate) component_access_set: FilteredAccessSet<ComponentId>,
// NOTE: this must be kept private. making a SystemMeta non-send is irreversible to prevent // NOTE: this must be kept private. making a SystemMeta non-send is irreversible to prevent
// SystemParams from overriding each other // SystemParams from overriding each other
is_send: bool, flags: SystemStateFlags,
has_deferred: bool,
pub(crate) last_run: Tick, pub(crate) last_run: Tick,
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
pub(crate) system_span: Span, pub(crate) system_span: Span,
@ -44,8 +45,7 @@ impl SystemMeta {
Self { Self {
name: name.into(), name: name.into(),
component_access_set: FilteredAccessSet::default(), component_access_set: FilteredAccessSet::default(),
is_send: true, flags: SystemStateFlags::empty(),
has_deferred: false,
last_run: Tick::new(0), last_run: Tick::new(0),
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
system_span: info_span!("system", name = name), system_span: info_span!("system", name = name),
@ -78,7 +78,7 @@ impl SystemMeta {
/// Returns true if the system is [`Send`]. /// Returns true if the system is [`Send`].
#[inline] #[inline]
pub fn is_send(&self) -> bool { pub fn is_send(&self) -> bool {
self.is_send !self.flags.intersects(SystemStateFlags::NON_SEND)
} }
/// Sets the system to be not [`Send`]. /// Sets the system to be not [`Send`].
@ -86,20 +86,20 @@ impl SystemMeta {
/// This is irreversible. /// This is irreversible.
#[inline] #[inline]
pub fn set_non_send(&mut self) { 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 /// Returns true if the system has deferred [`SystemParam`]'s
#[inline] #[inline]
pub fn has_deferred(&self) -> bool { 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`) /// Marks the system as having deferred buffers like [`Commands`](`super::Commands`)
/// This lets the scheduler insert [`ApplyDeferred`](`crate::prelude::ApplyDeferred`) systems automatically. /// This lets the scheduler insert [`ApplyDeferred`](`crate::prelude::ApplyDeferred`) systems automatically.
#[inline] #[inline]
pub fn set_has_deferred(&mut self) { pub fn set_has_deferred(&mut self) {
self.has_deferred = true; self.flags |= SystemStateFlags::DEFERRED;
} }
/// Returns a reference to the [`FilteredAccessSet`] for [`ComponentId`]. /// Returns a reference to the [`FilteredAccessSet`] for [`ComponentId`].
@ -631,18 +631,8 @@ where
} }
#[inline] #[inline]
fn is_send(&self) -> bool { fn flags(&self) -> SystemStateFlags {
self.system_meta.is_send self.system_meta.flags
}
#[inline]
fn is_exclusive(&self) -> bool {
false
}
#[inline]
fn has_deferred(&self) -> bool {
self.system_meta.has_deferred
} }
#[inline] #[inline]

View File

@ -127,18 +127,8 @@ where
} }
#[inline] #[inline]
fn is_send(&self) -> bool { fn flags(&self) -> super::SystemStateFlags {
self.observer.is_send() self.observer.flags()
}
#[inline]
fn is_exclusive(&self) -> bool {
self.observer.is_exclusive()
}
#[inline]
fn has_deferred(&self) -> bool {
self.observer.has_deferred()
} }
#[inline] #[inline]

View File

@ -2016,17 +2016,67 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
self.as_nop().get(entity).is_ok() 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>`. /// A transmute is valid only if `NewD` has a subset of the read, write, and required access
/// This can be useful for passing the query to another function. Note that since /// of the current query. A precise description of the access required by each parameter
/// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), /// type is given in the table below, but typical uses are to:
/// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be /// * Remove components, e.g. `Query<(&A, &B)>` to `Query<&A>`.
/// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] /// * Retrieve an existing component with reduced or equal access, e.g. `Query<&mut A>` to `Query<&A>`
/// or `Query<&T>` to `Query<Ref<T>>`.
/// * 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<T>`], [`PhantomData<T>`]|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<T>`]|Read and required access to `T`|
/// |`&mut T`, [`Mut<T>`]|Read, write and required access to `T`|
/// |[`Option<T>`], [`AnyOf<(D, ...)>`]|Read and write access to `T`, but no required access|
/// |Tuples of query data and<br/>`#[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<T>`], [`Changed<T>`]|Read and required access to `T`|
/// |[`With<T>`], [`Without<T>`]|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<T>`]: crate::query::Added
/// [`AnyOf<(D, ...)>`]: crate::query::AnyOf
/// [`&Archetype`]: crate::archetype::Archetype
/// [`Changed`]: crate::query::Changed
/// [`Changed<T>`]: crate::query::Changed
/// [`EntityMut`]: crate::world::EntityMut
/// [`EntityLocation`]: crate::entity::EntityLocation
/// [`EntityRef`]: crate::world::EntityRef
/// [`FilteredEntityRef`]: crate::world::FilteredEntityRef
/// [`FilteredEntityMut`]: crate::world::FilteredEntityMut
/// [`Has<T>`]: crate::query::Has
/// [`Mut<T>`]: crate::world::Mut
/// [`Or<(T, ...)>`]: crate::query::Or
/// [`QueryBuilder`]: crate::query::QueryBuilder
/// [`Ref<T>`]: crate::world::Ref
/// [`SpawnDetails`]: crate::query::SpawnDetails
/// [`Spawned`]: crate::query::Spawned
/// [`With<T>`]: crate::query::With
/// [`Without<T>`]: crate::query::Without
/// ///
/// ## Panics /// ## 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 /// ## Example
/// ///
@ -2065,30 +2115,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// # schedule.run(&mut world); /// # 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<T>`](crate::change_detection::Mut) have read, write, and required access to `T`
/// * `&T` and [`Ref<T>`](crate::change_detection::Ref) have read and required access to `T`
/// * [`Option<D>`] 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<T>`], and [`PhantomData<T>`] 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<T>`](crate::query::Added) and [`Changed<T>`](crate::query::Changed) filters have read and required access to `T`
/// * [`With<T>`](crate::query::With) and [`Without<T>`](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 /// ### Examples of valid transmutes
/// ///
/// ```rust /// ```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>`. /// // Nested inside of an `Or` filter, they have the same access as `Option<&T>`.
/// assert_valid_transmute_filtered::<Option<&T>, (), Entity, Or<(Changed<T>, With<U>)>>(); /// assert_valid_transmute_filtered::<Option<&T>, (), Entity, Or<(Changed<T>, With<U>)>>();
/// ``` /// ```
///
/// [`EntityLocation`]: crate::entity::EntityLocation
/// [`SpawnDetails`]: crate::query::SpawnDetails
/// [`&Archetype`]: crate::archetype::Archetype
/// [`Has<T>`]: crate::query::Has
#[track_caller] #[track_caller]
pub fn transmute_lens<NewD: QueryData>(&mut self) -> QueryLens<'_, NewD> { pub fn transmute_lens<NewD: QueryData>(&mut self) -> QueryLens<'_, NewD> {
self.transmute_lens_filtered::<NewD, ()>() self.transmute_lens_filtered::<NewD, ()>()
} }
/// 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. /// 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>`. /// See [`Self::transmute_lens`] for a description of allowed transmutes.
/// 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`]
/// ///
/// ## Panics /// ## 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 /// ## Example
/// ///
@ -2225,22 +2244,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// # schedule.run(&mut world); /// # 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>` <-> `&T`
/// * `&mut T` -> `&T`
/// * `&mut T` -> `Ref<T>`
/// * [`EntityMut`](crate::world::EntityMut) -> [`EntityRef`](crate::world::EntityRef)
///
/// [`EntityLocation`]: crate::entity::EntityLocation
/// [`&Archetype`]: crate::archetype::Archetype
///
/// # See also /// # See also
/// ///
/// - [`transmute_lens`](Self::transmute_lens) to convert to a lens using a mutable borrow of the [`Query`]. /// - [`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. /// 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 /// 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) /// 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), /// 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. /// 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. /// 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 /// 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) /// 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), /// 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 /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will only be respected if they
/// are in the type signature.
/// ///
/// # See also /// # See also
/// ///

View File

@ -8,7 +8,7 @@ use crate::{
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FromWorld, World}, 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`] /// A wrapper system to change a system that returns `()` to return `Ok(())` to make it into a [`ScheduleSystem`]
pub struct InfallibleSystemWrapper<S: System<In = ()>>(S); pub struct InfallibleSystemWrapper<S: System<In = ()>>(S);
@ -44,18 +44,8 @@ impl<S: System<In = ()>> System for InfallibleSystemWrapper<S> {
} }
#[inline] #[inline]
fn is_send(&self) -> bool { fn flags(&self) -> SystemStateFlags {
self.0.is_send() self.0.flags()
}
#[inline]
fn is_exclusive(&self) -> bool {
self.0.is_exclusive()
}
#[inline]
fn has_deferred(&self) -> bool {
self.0.has_deferred()
} }
#[inline] #[inline]
@ -172,16 +162,9 @@ where
self.system.component_access_set() self.system.component_access_set()
} }
fn is_send(&self) -> bool { #[inline]
self.system.is_send() fn flags(&self) -> SystemStateFlags {
} self.system.flags()
fn is_exclusive(&self) -> bool {
self.system.is_exclusive()
}
fn has_deferred(&self) -> bool {
self.system.has_deferred()
} }
unsafe fn run_unsafe( unsafe fn run_unsafe(
@ -281,16 +264,9 @@ where
self.system.component_access_set() self.system.component_access_set()
} }
fn is_send(&self) -> bool { #[inline]
self.system.is_send() fn flags(&self) -> SystemStateFlags {
} self.system.flags()
fn is_exclusive(&self) -> bool {
self.system.is_exclusive()
}
fn has_deferred(&self) -> bool {
self.system.has_deferred()
} }
unsafe fn run_unsafe( unsafe fn run_unsafe(

View File

@ -2,6 +2,7 @@
clippy::module_inception, clippy::module_inception,
reason = "This instance of module inception is being discussed; see #17353." reason = "This instance of module inception is being discussed; see #17353."
)] )]
use bitflags::bitflags;
use core::fmt::Debug; use core::fmt::Debug;
use log::warn; use log::warn;
use thiserror::Error; use thiserror::Error;
@ -19,6 +20,18 @@ use core::any::TypeId;
use super::{IntoSystem, SystemParamValidationError}; 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) /// An ECS system that can be added to a [`Schedule`](crate::schedule::Schedule)
/// ///
/// Systems are functions with all arguments implementing /// Systems are functions with all arguments implementing
@ -50,14 +63,26 @@ pub trait System: Send + Sync + 'static {
/// Returns the system's component [`FilteredAccessSet`]. /// Returns the system's component [`FilteredAccessSet`].
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId>; fn component_access_set(&self) -> &FilteredAccessSet<ComponentId>;
/// Returns the [`SystemStateFlags`] of the system.
fn flags(&self) -> SystemStateFlags;
/// Returns true if the system is [`Send`]. /// 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. /// 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. /// 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 /// 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 /// can be called in parallel with other systems and may break Rust's aliasing rules

View File

@ -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. /// 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> { 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>, world: UnsafeWorldCell<'w>,
} }
@ -157,7 +157,7 @@ impl<'w> DeferredWorld<'w> {
if archetype.has_replace_observer() { if archetype.has_replace_observer() {
self.trigger_observers( self.trigger_observers(
ON_REPLACE, ON_REPLACE,
entity, Some(entity),
[component_id].into_iter(), [component_id].into_iter(),
MaybeLocation::caller(), MaybeLocation::caller(),
); );
@ -197,7 +197,7 @@ impl<'w> DeferredWorld<'w> {
if archetype.has_insert_observer() { if archetype.has_insert_observer() {
self.trigger_observers( self.trigger_observers(
ON_INSERT, ON_INSERT,
entity, Some(entity),
[component_id].into_iter(), [component_id].into_iter(),
MaybeLocation::caller(), MaybeLocation::caller(),
); );
@ -738,7 +738,7 @@ impl<'w> DeferredWorld<'w> {
pub(crate) unsafe fn trigger_observers( pub(crate) unsafe fn trigger_observers(
&mut self, &mut self,
event: ComponentId, event: ComponentId,
target: Entity, target: Option<Entity>,
components: impl Iterator<Item = ComponentId> + Clone, components: impl Iterator<Item = ComponentId> + Clone,
caller: MaybeLocation, caller: MaybeLocation,
) { ) {
@ -761,7 +761,7 @@ impl<'w> DeferredWorld<'w> {
pub(crate) unsafe fn trigger_observers_with_data<E, T>( pub(crate) unsafe fn trigger_observers_with_data<E, T>(
&mut self, &mut self,
event: ComponentId, event: ComponentId,
mut target: Entity, target: Option<Entity>,
components: impl Iterator<Item = ComponentId> + Clone, components: impl Iterator<Item = ComponentId> + Clone,
data: &mut E, data: &mut E,
mut propagate: bool, mut propagate: bool,
@ -769,7 +769,6 @@ impl<'w> DeferredWorld<'w> {
) where ) where
T: Traversal<E>, T: Traversal<E>,
{ {
loop {
Observers::invoke::<_>( Observers::invoke::<_>(
self.reborrow(), self.reborrow(),
event, event,
@ -779,8 +778,11 @@ impl<'w> DeferredWorld<'w> {
&mut propagate, &mut propagate,
caller, caller,
); );
let Some(mut target) = target else { return };
loop {
if !propagate { if !propagate {
break; return;
} }
if let Some(traverse_to) = self if let Some(traverse_to) = self
.get_entity(target) .get_entity(target)
@ -792,6 +794,15 @@ impl<'w> DeferredWorld<'w> {
} else { } else {
break; break;
} }
Observers::invoke::<_>(
self.reborrow(),
event,
Some(target),
components.clone(),
data,
&mut propagate,
caller,
);
} }
} }

View File

@ -2371,7 +2371,7 @@ impl<'w> EntityWorldMut<'w> {
if archetype.has_despawn_observer() { if archetype.has_despawn_observer() {
deferred_world.trigger_observers( deferred_world.trigger_observers(
ON_DESPAWN, ON_DESPAWN,
self.entity, Some(self.entity),
archetype.components(), archetype.components(),
caller, caller,
); );
@ -2385,7 +2385,7 @@ impl<'w> EntityWorldMut<'w> {
if archetype.has_replace_observer() { if archetype.has_replace_observer() {
deferred_world.trigger_observers( deferred_world.trigger_observers(
ON_REPLACE, ON_REPLACE,
self.entity, Some(self.entity),
archetype.components(), archetype.components(),
caller, caller,
); );
@ -2400,7 +2400,7 @@ impl<'w> EntityWorldMut<'w> {
if archetype.has_remove_observer() { if archetype.has_remove_observer() {
deferred_world.trigger_observers( deferred_world.trigger_observers(
ON_REMOVE, ON_REMOVE,
self.entity, Some(self.entity),
archetype.components(), archetype.components(),
caller, caller,
); );
@ -5749,7 +5749,9 @@ mod tests {
let entity = world let entity = world
.spawn_empty() .spawn_empty()
.observe(|trigger: Trigger<TestEvent>, mut commands: Commands| { .observe(|trigger: Trigger<TestEvent>, mut commands: Commands| {
commands.entity(trigger.target()).insert(TestComponent(0)); commands
.entity(trigger.target().unwrap())
.insert(TestComponent(0));
}) })
.id(); .id();
@ -5769,7 +5771,7 @@ mod tests {
let mut world = World::new(); let mut world = World::new();
world.add_observer( world.add_observer(
|trigger: Trigger<OnAdd, TestComponent>, mut commands: Commands| { |trigger: Trigger<OnAdd, TestComponent>, mut commands: Commands| {
commands.entity(trigger.target()).despawn(); commands.entity(trigger.target().unwrap()).despawn();
}, },
); );
let entity = world.spawn_empty().id(); let entity = world.spawn_empty().id();

View File

@ -81,19 +81,35 @@ impl ImageLoader {
} }
} }
/// How to determine an image's format when loading.
#[derive(Serialize, Deserialize, Default, Debug, Clone)] #[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub enum ImageFormatSetting { pub enum ImageFormatSetting {
/// Determine the image format from its file extension.
///
/// This is the default.
#[default] #[default]
FromExtension, FromExtension,
/// Declare the image format explicitly.
Format(ImageFormat), Format(ImageFormat),
/// Guess the image format by looking for magic bytes at the
/// beginning of its data.
Guess, Guess,
} }
/// Settings for loading an [`Image`] using an [`ImageLoader`].
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ImageLoaderSettings { pub struct ImageLoaderSettings {
/// How to determine the image's format.
pub format: ImageFormatSetting, 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, pub is_srgb: bool,
/// [`ImageSampler`] to use when rendering - this does
/// not affect the loading of the image data.
pub sampler: ImageSampler, pub sampler: ImageSampler,
/// Where the asset will be used - see the docs on
/// [`RenderAssetUsages`] for details.
pub asset_usage: RenderAssetUsages, pub asset_usage: RenderAssetUsages,
} }
@ -108,11 +124,14 @@ impl Default for ImageLoaderSettings {
} }
} }
/// An error when loading an image using [`ImageLoader`].
#[non_exhaustive] #[non_exhaustive]
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ImageLoaderError { 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), Io(#[from] std::io::Error),
/// An error occurred while trying to decode the image bytes.
#[error("Could not load texture file: {0}")] #[error("Could not load texture file: {0}")]
FileTexture(#[from] FileTextureError), FileTexture(#[from] FileTextureError),
} }
@ -170,7 +189,7 @@ impl AssetLoader for ImageLoader {
/// An error that occurs when loading a texture from a file. /// An error that occurs when loading a texture from a file.
#[derive(Error, Debug)] #[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 { pub struct FileTextureError {
error: TextureError, error: TextureError,
path: String, path: String,

View File

@ -394,7 +394,7 @@ mod tests {
trigger: Trigger<FocusedInput<KeyboardInput>>, trigger: Trigger<FocusedInput<KeyboardInput>>,
mut query: Query<&mut GatherKeyboardEvents>, 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 { if let Key::Character(c) = &trigger.input.logical_key {
gather.0.push_str(c.as_str()); gather.0.push_str(c.as_str());
} }

View File

@ -551,7 +551,7 @@ pub(crate) fn add_light_view_entities(
trigger: Trigger<OnAdd, (ExtractedDirectionalLight, ExtractedPointLight)>, trigger: Trigger<OnAdd, (ExtractedDirectionalLight, ExtractedPointLight)>,
mut commands: Commands, 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()); v.insert(LightViewEntities::default());
} }
} }
@ -561,7 +561,7 @@ pub(crate) fn extracted_light_removed(
trigger: Trigger<OnRemove, (ExtractedDirectionalLight, ExtractedPointLight)>, trigger: Trigger<OnRemove, (ExtractedDirectionalLight, ExtractedPointLight)>,
mut commands: Commands, 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::<LightViewEntities>(); v.try_remove::<LightViewEntities>();
} }
} }
@ -571,7 +571,7 @@ pub(crate) fn remove_light_view_entities(
query: Query<&LightViewEntities>, query: Query<&LightViewEntities>,
mut commands: Commands, 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 v in entities.0.values() {
for e in v.iter().copied() { for e in v.iter().copied() {
if let Ok(mut v) = commands.get_entity(e) { if let Ok(mut v) = commands.get_entity(e) {

View File

@ -208,18 +208,6 @@ pub fn update_interactions(
mut pointers: Query<(&PointerId, &PointerPress, &mut PointerInteraction)>, mut pointers: Query<(&PointerId, &PointerPress, &mut PointerInteraction)>,
mut interact: Query<&mut PickingInteraction>, 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 // 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 // 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 // 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. // 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) { 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) { } else if let Ok(mut entity_commands) = commands.get_entity(hovered_entity) {
entity_commands.try_insert(new_interaction); 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. /// Merge the interaction state of this entity into the aggregated map.

View File

@ -55,13 +55,13 @@
//! // Spawn your entity here, e.g. a Mesh. //! // Spawn your entity here, e.g. a Mesh.
//! // When dragged, mutate the `Transform` component on the dragged target entity: //! // When dragged, mutate the `Transform` component on the dragged target entity:
//! .observe(|trigger: Trigger<Pointer<Drag>>, mut transforms: Query<&mut Transform>| { //! .observe(|trigger: Trigger<Pointer<Drag>>, 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(); //! let drag = trigger.event();
//! transform.rotate_local_y(drag.delta.x / 50.0); //! transform.rotate_local_y(drag.delta.x / 50.0);
//! }) //! })
//! .observe(|trigger: Trigger<Pointer<Click>>, mut commands: Commands| { //! .observe(|trigger: Trigger<Pointer<Click>>, mut commands: Commands| {
//! println!("Entity {} goes BOOM!", trigger.target()); //! println!("Entity {} goes BOOM!", trigger.target().unwrap());
//! commands.entity(trigger.target()).despawn(); //! commands.entity(trigger.target().unwrap()).despawn();
//! }) //! })
//! .observe(|trigger: Trigger<Pointer<Over>>, mut events: EventWriter<Greeting>| { //! .observe(|trigger: Trigger<Pointer<Over>>, mut events: EventWriter<Greeting>| {
//! events.write(Greeting); //! events.write(Greeting);

View File

@ -119,7 +119,7 @@ wesl = { version = "0.1.2", optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# Omit the `glsl` feature in non-WebAssembly by default. # 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", "test_shader",
] } ] }
@ -127,7 +127,7 @@ naga_oil = { version = "0.17", default-features = false, features = [
proptest = "1" proptest = "1"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
naga_oil = "0.17" naga_oil = "0.17.1"
js-sys = "0.3" js-sys = "0.3"
web-sys = { version = "0.3.67", features = [ web-sys = { version = "0.3.67", features = [
'Blob', 'Blob',

View File

@ -1060,6 +1060,7 @@ pub fn camera_system(
#[reflect(opaque)] #[reflect(opaque)]
#[reflect(Component, Default, Clone)] #[reflect(Component, Default, Clone)]
pub struct CameraMainTextureUsages(pub TextureUsages); pub struct CameraMainTextureUsages(pub TextureUsages);
impl Default for CameraMainTextureUsages { impl Default for CameraMainTextureUsages {
fn default() -> Self { fn default() -> Self {
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)] #[derive(Component, Debug)]
pub struct ExtractedCamera { pub struct ExtractedCamera {
pub target: Option<NormalizedRenderTarget>, pub target: Option<NormalizedRenderTarget>,

View File

@ -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] { impl<'a> IntoBinding<'a> for &'a [&'a wgpu::TextureView] {
#[inline] #[inline]
fn into_binding(self) -> BindingResource<'a> { 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> { impl<'a> IntoBinding<'a> for BindingResource<'a> {
#[inline] #[inline]
fn into_binding(self) -> BindingResource<'a> { 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> { pub trait IntoBindingArray<'b, const N: usize> {
fn into_array(self) -> [BindingResource<'b>; N]; fn into_array(self) -> [BindingResource<'b>; N];
} }

View File

@ -568,4 +568,8 @@ pub mod binding_types {
} }
.into_bind_group_layout_entry_builder() .into_bind_group_layout_entry_builder()
} }
pub fn acceleration_structure() -> BindGroupLayoutEntryBuilder {
BindingType::AccelerationStructure.into_bind_group_layout_entry_builder()
}
} }

View File

@ -38,18 +38,21 @@ pub use wgpu::{
BufferInitDescriptor, DispatchIndirectArgs, DrawIndexedIndirectArgs, DrawIndirectArgs, BufferInitDescriptor, DispatchIndirectArgs, DrawIndexedIndirectArgs, DrawIndirectArgs,
TextureDataOrder, TextureDataOrder,
}, },
AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, AstcChannel, BindGroupDescriptor, AccelerationStructureFlags, AccelerationStructureGeometryFlags,
BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, 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, BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferAsyncError,
BufferBinding, BufferBindingType, BufferDescriptor, BufferSize, BufferUsages, ColorTargetState, BufferBinding, BufferBindingType, BufferDescriptor, BufferSize, BufferUsages, ColorTargetState,
ColorWrites, CommandEncoder, CommandEncoderDescriptor, CompareFunction, ComputePass, ColorWrites, CommandEncoder, CommandEncoderDescriptor, CompareFunction, ComputePass,
ComputePassDescriptor, ComputePipelineDescriptor as RawComputePipelineDescriptor, ComputePassDescriptor, ComputePipelineDescriptor as RawComputePipelineDescriptor,
DepthBiasState, DepthStencilState, DownlevelFlags, Extent3d, Face, Features as WgpuFeatures, CreateBlasDescriptor, CreateTlasDescriptor, DepthBiasState, DepthStencilState, DownlevelFlags,
FilterMode, FragmentState as RawFragmentState, FrontFace, ImageSubresourceRange, IndexFormat, Extent3d, Face, Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState,
Limits as WgpuLimits, LoadOp, Maintain, MapMode, MultisampleState, Operations, Origin3d, FrontFace, ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, Maintain, MapMode,
PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PolygonMode, MultisampleState, Operations, Origin3d, PipelineCompilationOptions, PipelineLayout,
PrimitiveState, PrimitiveTopology, PushConstantRange, RenderPassColorAttachment, PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, PushConstantRange,
RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor,
RenderPipelineDescriptor as RawRenderPipelineDescriptor, Sampler as WgpuSampler, RenderPipelineDescriptor as RawRenderPipelineDescriptor, Sampler as WgpuSampler,
SamplerBindingType, SamplerBindingType as WgpuSamplerBindingType, SamplerDescriptor, SamplerBindingType, SamplerBindingType as WgpuSamplerBindingType, SamplerDescriptor,
ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState,
@ -57,8 +60,9 @@ pub use wgpu::{
TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, TextureDescriptor, TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, TextureDescriptor,
TextureDimension, TextureFormat, TextureFormatFeatureFlags, TextureFormatFeatures, TextureDimension, TextureFormat, TextureFormatFeatureFlags, TextureFormatFeatures,
TextureSampleType, TextureUsages, TextureView as WgpuTextureView, TextureViewDescriptor, TextureSampleType, TextureUsages, TextureView as WgpuTextureView, TextureViewDescriptor,
TextureViewDimension, VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, TextureViewDimension, Tlas, TlasInstance, TlasPackage, VertexAttribute,
VertexFormat, VertexState as RawVertexState, VertexStepMode, COPY_BUFFER_ALIGNMENT, VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState,
VertexStepMode, COPY_BUFFER_ALIGNMENT,
}; };
pub use crate::mesh::VertexBufferLayout; pub use crate::mesh::VertexBufferLayout;

View File

@ -94,14 +94,14 @@ impl Plugin for SyncWorldPlugin {
app.init_resource::<PendingSyncEntity>(); app.init_resource::<PendingSyncEntity>();
app.add_observer( app.add_observer(
|trigger: Trigger<OnAdd, SyncToRenderWorld>, mut pending: ResMut<PendingSyncEntity>| { |trigger: Trigger<OnAdd, SyncToRenderWorld>, mut pending: ResMut<PendingSyncEntity>| {
pending.push(EntityRecord::Added(trigger.target())); pending.push(EntityRecord::Added(trigger.target().unwrap()));
}, },
); );
app.add_observer( app.add_observer(
|trigger: Trigger<OnRemove, SyncToRenderWorld>, |trigger: Trigger<OnRemove, SyncToRenderWorld>,
mut pending: ResMut<PendingSyncEntity>, mut pending: ResMut<PendingSyncEntity>,
query: Query<&RenderEntity>| { 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)); pending.push(EntityRecord::Removed(*e));
}; };
}, },
@ -512,14 +512,14 @@ mod tests {
main_world.add_observer( main_world.add_observer(
|trigger: Trigger<OnAdd, SyncToRenderWorld>, mut pending: ResMut<PendingSyncEntity>| { |trigger: Trigger<OnAdd, SyncToRenderWorld>, mut pending: ResMut<PendingSyncEntity>| {
pending.push(EntityRecord::Added(trigger.target())); pending.push(EntityRecord::Added(trigger.target().unwrap()));
}, },
); );
main_world.add_observer( main_world.add_observer(
|trigger: Trigger<OnRemove, SyncToRenderWorld>, |trigger: Trigger<OnRemove, SyncToRenderWorld>,
mut pending: ResMut<PendingSyncEntity>, mut pending: ResMut<PendingSyncEntity>,
query: Query<&RenderEntity>| { 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)); pending.push(EntityRecord::Removed(*e));
}; };
}, },

View File

@ -721,7 +721,7 @@ mod tests {
.expect("Failed to run dynamic scene builder system.") .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<Entity>) {
// Add observer // Add observer
app.world_mut().add_observer( app.world_mut().add_observer(
move |trigger: Trigger<SceneInstanceReady>, move |trigger: Trigger<SceneInstanceReady>,
@ -773,7 +773,7 @@ mod tests {
.unwrap(); .unwrap();
// Check trigger. // Check trigger.
observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); observe_trigger(&mut app, scene_id, None);
} }
#[test] #[test]
@ -792,7 +792,7 @@ mod tests {
.unwrap(); .unwrap();
// Check trigger. // Check trigger.
observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); observe_trigger(&mut app, scene_id, None);
} }
#[test] #[test]
@ -816,7 +816,7 @@ mod tests {
.unwrap(); .unwrap();
// Check trigger. // Check trigger.
observe_trigger(&mut app, scene_id, scene_entity); observe_trigger(&mut app, scene_id, Some(scene_entity));
} }
#[test] #[test]
@ -840,7 +840,7 @@ mod tests {
.unwrap(); .unwrap();
// Check trigger. // Check trigger.
observe_trigger(&mut app, scene_id, scene_entity); observe_trigger(&mut app, scene_id, Some(scene_entity));
} }
#[test] #[test]

View File

@ -1,6 +1,7 @@
use crate::{ use crate::{
experimental::UiChildren, experimental::UiChildren,
prelude::{Button, Label}, prelude::{Button, Label},
ui_transform::UiGlobalTransform,
widget::{ImageNode, TextUiReader}, widget::{ImageNode, TextUiReader},
ComputedNode, ComputedNode,
}; };
@ -13,11 +14,9 @@ use bevy_ecs::{
system::{Commands, Query}, system::{Commands, Query},
world::Ref, world::Ref,
}; };
use bevy_math::Vec3Swizzles;
use bevy_render::camera::CameraUpdateSystems;
use bevy_transform::prelude::GlobalTransform;
use accesskit::{Node, Rect, Role}; use accesskit::{Node, Rect, Role};
use bevy_render::camera::CameraUpdateSystems;
fn calc_label( fn calc_label(
text_reader: &mut TextUiReader, text_reader: &mut TextUiReader,
@ -40,12 +39,12 @@ fn calc_bounds(
mut nodes: Query<( mut nodes: Query<(
&mut AccessibilityNode, &mut AccessibilityNode,
Ref<ComputedNode>, Ref<ComputedNode>,
Ref<GlobalTransform>, Ref<UiGlobalTransform>,
)>, )>,
) { ) {
for (mut accessible, node, transform) in &mut nodes { for (mut accessible, node, transform) in &mut nodes {
if node.is_changed() || transform.is_changed() { if node.is_changed() || transform.is_changed() {
let center = transform.translation().xy(); let center = transform.translation;
let half_size = 0.5 * node.size; let half_size = 0.5 * node.size;
let min = center - half_size; let min = center - half_size;
let max = center + half_size; let max = center + half_size;

View File

@ -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::{ use bevy_ecs::{
change_detection::DetectChangesMut, change_detection::DetectChangesMut,
entity::{ContainsEntity, Entity}, entity::{ContainsEntity, Entity},
hierarchy::ChildOf,
prelude::{Component, With}, prelude::{Component, With},
query::QueryData, query::QueryData,
reflect::ReflectComponent, reflect::ReflectComponent,
system::{Local, Query, Res}, system::{Local, Query, Res},
}; };
use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput}; use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput};
use bevy_math::{Rect, Vec2}; use bevy_math::Vec2;
use bevy_platform::collections::HashMap; use bevy_platform::collections::HashMap;
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::InheritedVisibility}; use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::InheritedVisibility};
use bevy_transform::components::GlobalTransform;
use bevy_window::{PrimaryWindow, Window}; use bevy_window::{PrimaryWindow, Window};
use smallvec::SmallVec; 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 /// 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., 0.) to (1., 1.) /// 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. /// 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)] #[derive(Component, Copy, Clone, Default, PartialEq, Debug, Reflect)]
#[reflect(Component, Default, PartialEq, Debug, Clone)] #[reflect(Component, Default, PartialEq, Debug, Clone)]
#[cfg_attr( #[cfg_attr(
@ -81,8 +84,8 @@ impl Default for Interaction {
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub struct RelativeCursorPosition { pub struct RelativeCursorPosition {
/// Visible area of the Node relative to the size of the entire Node. /// True if the cursor position is over an unclipped area of the Node.
pub normalized_visible_node_rect: Rect, pub cursor_over: bool,
/// Cursor position relative to the size and position of the Node. /// Cursor position relative to the size and position of the Node.
/// A None value indicates that the cursor position is unknown. /// A None value indicates that the cursor position is unknown.
pub normalized: Option<Vec2>, pub normalized: Option<Vec2>,
@ -90,9 +93,8 @@ pub struct RelativeCursorPosition {
impl RelativeCursorPosition { impl RelativeCursorPosition {
/// A helper function to check if the mouse is over the node /// A helper function to check if the mouse is over the node
pub fn mouse_over(&self) -> bool { pub fn cursor_over(&self) -> bool {
self.normalized self.cursor_over
.is_some_and(|position| self.normalized_visible_node_rect.contains(position))
} }
} }
@ -133,11 +135,10 @@ pub struct State {
pub struct NodeQuery { pub struct NodeQuery {
entity: Entity, entity: Entity,
node: &'static ComputedNode, node: &'static ComputedNode,
global_transform: &'static GlobalTransform, transform: &'static UiGlobalTransform,
interaction: Option<&'static mut Interaction>, interaction: Option<&'static mut Interaction>,
relative_cursor_position: Option<&'static mut RelativeCursorPosition>, relative_cursor_position: Option<&'static mut RelativeCursorPosition>,
focus_policy: Option<&'static FocusPolicy>, focus_policy: Option<&'static FocusPolicy>,
calculated_clip: Option<&'static CalculatedClip>,
inherited_visibility: Option<&'static InheritedVisibility>, inherited_visibility: Option<&'static InheritedVisibility>,
target_camera: &'static ComputedNodeTarget, target_camera: &'static ComputedNodeTarget,
} }
@ -154,6 +155,8 @@ pub fn ui_focus_system(
touches_input: Res<Touches>, touches_input: Res<Touches>,
ui_stack: Res<UiStack>, ui_stack: Res<UiStack>,
mut node_query: Query<NodeQuery>, mut node_query: Query<NodeQuery>,
clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>,
child_of_query: Query<&ChildOf>,
) { ) {
let primary_window = primary_window.iter().next(); let primary_window = primary_window.iter().next();
@ -234,46 +237,30 @@ pub fn ui_focus_system(
} }
let camera_entity = node.target_camera.camera()?; 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 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 // 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. // 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 // 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 // +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to
// false positives for mouse_over (#12395) // false positives for mouse_over (#12395)
(node_rect.size().cmpgt(Vec2::ZERO).all()) node.node.normalize_point(*node.transform, *cursor_position)
.then_some((*cursor_position - node_rect.min) / node_rect.size())
}); });
// If the current cursor position is within the bounds of the node's visible area, consider it for // If the current cursor position is within the bounds of the node's visible area, consider it for
// clicking // clicking
let relative_cursor_position_component = RelativeCursorPosition { let relative_cursor_position_component = RelativeCursorPosition {
normalized_visible_node_rect: visible_rect.normalize(node_rect), cursor_over: contains_cursor,
normalized: relative_cursor_position, 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 // Save the relative cursor position to the correct component
if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position
{ {
@ -284,7 +271,8 @@ pub fn ui_focus_system(
Some(*entity) Some(*entity)
} else { } else {
if let Some(mut interaction) = node.interaction { 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); 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.
}

View File

@ -448,6 +448,8 @@ impl RepeatedGridTrack {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bevy_math::Vec2;
use super::*; use super::*;
#[test] #[test]
@ -523,7 +525,7 @@ mod tests {
grid_column: GridPlacement::start(4), grid_column: GridPlacement::start(4),
grid_row: GridPlacement::span(3), 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); let taffy_style = from_node(&node, &viewport_values, false);
assert_eq!(taffy_style.display, taffy::style::Display::Flex); assert_eq!(taffy_style.display, taffy::style::Display::Flex);
assert_eq!(taffy_style.box_sizing, taffy::style::BoxSizing::ContentBox); assert_eq!(taffy_style.box_sizing, taffy::style::BoxSizing::ContentBox);
@ -661,7 +663,7 @@ mod tests {
#[test] #[test]
fn test_into_length_percentage() { fn test_into_length_percentage() {
use taffy::style::LengthPercentage; 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 = [ let cases = [
(Val::Auto, LengthPercentage::Length(0.)), (Val::Auto, LengthPercentage::Length(0.)),
(Val::Percent(1.), LengthPercentage::Percent(0.01)), (Val::Percent(1.), LengthPercentage::Percent(0.01)),

View File

@ -1,5 +1,6 @@
use crate::{ use crate::{
experimental::{UiChildren, UiRootNodes}, experimental::{UiChildren, UiRootNodes},
ui_transform::{UiGlobalTransform, UiTransform},
BorderRadius, ComputedNode, ComputedNodeTarget, ContentSize, Display, LayoutConfig, Node, BorderRadius, ComputedNode, ComputedNodeTarget, ContentSize, Display, LayoutConfig, Node,
Outline, OverflowAxis, ScrollPosition, Outline, OverflowAxis, ScrollPosition,
}; };
@ -12,9 +13,9 @@ use bevy_ecs::{
system::{Commands, Query, ResMut}, system::{Commands, Query, ResMut},
world::Ref, world::Ref,
}; };
use bevy_math::Vec2;
use bevy_math::{Affine2, Vec2};
use bevy_sprite::BorderRect; use bevy_sprite::BorderRect;
use bevy_transform::components::Transform;
use thiserror::Error; use thiserror::Error;
use tracing::warn; use tracing::warn;
use ui_surface::UiSurface; use ui_surface::UiSurface;
@ -81,9 +82,10 @@ pub fn ui_layout_system(
)>, )>,
computed_node_query: Query<(Entity, Option<Ref<ChildOf>>), With<ComputedNode>>, computed_node_query: Query<(Entity, Option<Ref<ChildOf>>), With<ComputedNode>>,
ui_children: UiChildren, ui_children: UiChildren,
mut node_transform_query: Query<( mut node_update_query: Query<(
&mut ComputedNode, &mut ComputedNode,
&mut Transform, &UiTransform,
&mut UiGlobalTransform,
&Node, &Node,
Option<&LayoutConfig>, Option<&LayoutConfig>,
Option<&BorderRadius>, Option<&BorderRadius>,
@ -175,7 +177,8 @@ with UI components as a child of an entity without UI components, your UI layout
&mut ui_surface, &mut ui_surface,
true, true,
computed_target.physical_size().as_vec2(), computed_target.physical_size().as_vec2(),
&mut node_transform_query, Affine2::IDENTITY,
&mut node_update_query,
&ui_children, &ui_children,
computed_target.scale_factor.recip(), computed_target.scale_factor.recip(),
Vec2::ZERO, 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, ui_surface: &mut UiSurface,
inherited_use_rounding: bool, inherited_use_rounding: bool,
target_size: Vec2, target_size: Vec2,
node_transform_query: &mut Query<( mut inherited_transform: Affine2,
node_update_query: &mut Query<(
&mut ComputedNode, &mut ComputedNode,
&mut Transform, &UiTransform,
&mut UiGlobalTransform,
&Node, &Node,
Option<&LayoutConfig>, Option<&LayoutConfig>,
Option<&BorderRadius>, Option<&BorderRadius>,
@ -206,13 +211,14 @@ with UI components as a child of an entity without UI components, your UI layout
) { ) {
if let Ok(( if let Ok((
mut node, mut node,
mut transform, transform,
mut global_transform,
style, style,
maybe_layout_config, maybe_layout_config,
maybe_border_radius, maybe_border_radius,
maybe_outline, maybe_outline,
maybe_scroll_position, maybe_scroll_position,
)) = node_transform_query.get_mut(entity) )) = node_update_query.get_mut(entity)
{ {
let use_rounding = maybe_layout_config let use_rounding = maybe_layout_config
.map(|layout_config| layout_config.use_rounding) .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); 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); 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 // The position of the center of the node relative to its top-left corner.
let node_center = let local_center =
layout_location - parent_scroll_position + 0.5 * (layout_size - parent_size); layout_location - parent_scroll_position + 0.5 * (layout_size - parent_size);
// only trigger change detection when the new values are different // 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().border = taffy_rect_to_border_rect(layout.border);
node.bypass_change_detection().padding = taffy_rect_to_border_rect(layout.padding); 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 { if let Some(border_radius) = maybe_border_radius {
// We don't trigger change detection for changes to border radius // We don't trigger change detection for changes to border radius
node.bypass_change_detection().border_radius = border_radius.resolve( 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.); .max(0.);
} }
if transform.translation.truncate() != node_center {
transform.translation = node_center.extend(0.);
}
let scroll_position: Vec2 = maybe_scroll_position let scroll_position: Vec2 = maybe_scroll_position
.map(|scroll_pos| { .map(|scroll_pos| {
Vec2::new( Vec2::new(
@ -333,7 +346,8 @@ with UI components as a child of an entity without UI components, your UI layout
ui_surface, ui_surface,
use_rounding, use_rounding,
target_size, target_size,
node_transform_query, inherited_transform,
node_update_query,
ui_children, ui_children,
inverse_target_scale_factor, inverse_target_scale_factor,
layout_size, layout_size,
@ -356,10 +370,7 @@ mod tests {
use bevy_platform::collections::HashMap; use bevy_platform::collections::HashMap;
use bevy_render::{camera::ManualTextureViews, prelude::Camera}; use bevy_render::{camera::ManualTextureViews, prelude::Camera};
use bevy_transform::systems::mark_dirty_trees; use bevy_transform::systems::mark_dirty_trees;
use bevy_transform::{ use bevy_transform::systems::{propagate_parent_transforms, sync_simple_transforms};
prelude::GlobalTransform,
systems::{propagate_parent_transforms, sync_simple_transforms},
};
use bevy_utils::prelude::default; use bevy_utils::prelude::default;
use bevy_window::{ use bevy_window::{
PrimaryWindow, Window, WindowCreated, WindowResized, WindowResolution, PrimaryWindow, Window, WindowCreated, WindowResized, WindowResolution,
@ -684,23 +695,20 @@ mod tests {
ui_schedule.run(&mut world); ui_schedule.run(&mut world);
let overlap_check = world let overlap_check = world
.query_filtered::<(Entity, &ComputedNode, &GlobalTransform), Without<ChildOf>>() .query_filtered::<(Entity, &ComputedNode, &UiGlobalTransform), Without<ChildOf>>()
.iter(&world) .iter(&world)
.fold( .fold(
Option::<(Rect, bool)>::None, Option::<(Rect, bool)>::None,
|option_rect, (entity, node, global_transform)| { |option_rect, (entity, node, transform)| {
let current_rect = Rect::from_center_size( let current_rect = Rect::from_center_size(transform.translation, node.size());
global_transform.translation().truncate(),
node.size(),
);
assert!( assert!(
current_rect.height().abs() + current_rect.width().abs() > 0., current_rect.height().abs() + current_rect.width().abs() > 0.,
"root ui node {entity} doesn't have a logical size" "root ui node {entity} doesn't have a logical size"
); );
assert_ne!( assert_ne!(
global_transform.affine(), *transform,
GlobalTransform::default().affine(), UiGlobalTransform::default(),
"root ui node {entity} global transform is not populated" "root ui node {entity} transform is not populated"
); );
let Some((rect, is_overlapping)) = option_rect else { let Some((rect, is_overlapping)) = option_rect else {
return Some((current_rect, false)); return Some((current_rect, false));

View File

@ -18,6 +18,7 @@ pub mod widget;
pub mod gradients; pub mod gradients;
#[cfg(feature = "bevy_ui_picking_backend")] #[cfg(feature = "bevy_ui_picking_backend")]
pub mod picking_backend; pub mod picking_backend;
pub mod ui_transform;
use bevy_derive::{Deref, DerefMut}; use bevy_derive::{Deref, DerefMut};
#[cfg(feature = "bevy_ui_picking_backend")] #[cfg(feature = "bevy_ui_picking_backend")]
@ -42,6 +43,7 @@ pub use measurement::*;
pub use render::*; pub use render::*;
pub use ui_material::*; pub use ui_material::*;
pub use ui_node::*; pub use ui_node::*;
pub use ui_transform::*;
use widget::{ImageNode, ImageNodeSize, ViewportNode}; use widget::{ImageNode, ImageNodeSize, ViewportNode};
@ -64,6 +66,7 @@ pub mod prelude {
gradients::*, gradients::*,
ui_material::*, ui_material::*,
ui_node::*, ui_node::*,
ui_transform::*,
widget::{Button, ImageNode, Label, NodeImageMode, ViewportNode}, widget::{Button, ImageNode, Label, NodeImageMode, ViewportNode},
Interaction, MaterialNode, UiMaterialPlugin, UiScale, Interaction, MaterialNode, UiMaterialPlugin, UiScale,
}, },

View File

@ -24,14 +24,13 @@
#![deny(missing_docs)] #![deny(missing_docs)]
use crate::{focus::pick_rounded_rect, prelude::*, UiStack}; use crate::{prelude::*, ui_transform::UiGlobalTransform, UiStack};
use bevy_app::prelude::*; use bevy_app::prelude::*;
use bevy_ecs::{prelude::*, query::QueryData}; use bevy_ecs::{prelude::*, query::QueryData};
use bevy_math::{Rect, Vec2}; use bevy_math::Vec2;
use bevy_platform::collections::HashMap; use bevy_platform::collections::HashMap;
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::prelude::*; use bevy_render::prelude::*;
use bevy_transform::prelude::*;
use bevy_window::PrimaryWindow; use bevy_window::PrimaryWindow;
use bevy_picking::backend::prelude::*; use bevy_picking::backend::prelude::*;
@ -91,9 +90,8 @@ impl Plugin for UiPickingPlugin {
pub struct NodeQuery { pub struct NodeQuery {
entity: Entity, entity: Entity,
node: &'static ComputedNode, node: &'static ComputedNode,
global_transform: &'static GlobalTransform, transform: &'static UiGlobalTransform,
pickable: Option<&'static Pickable>, pickable: Option<&'static Pickable>,
calculated_clip: Option<&'static CalculatedClip>,
inherited_visibility: Option<&'static InheritedVisibility>, inherited_visibility: Option<&'static InheritedVisibility>,
target_camera: &'static ComputedNodeTarget, target_camera: &'static ComputedNodeTarget,
} }
@ -110,6 +108,8 @@ pub fn ui_picking(
ui_stack: Res<UiStack>, ui_stack: Res<UiStack>,
node_query: Query<NodeQuery>, node_query: Query<NodeQuery>,
mut output: EventWriter<PointerHits>, mut output: EventWriter<PointerHits>,
clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>,
child_of_query: Query<&ChildOf>,
) { ) {
// For each camera, the pointer and its position // For each camera, the pointer and its position
let mut pointer_pos_by_camera = HashMap::<Entity, HashMap<PointerId, Vec2>>::default(); let mut pointer_pos_by_camera = HashMap::<Entity, HashMap<PointerId, Vec2>>::default();
@ -181,43 +181,33 @@ pub fn ui_picking(
continue; 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 // 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; 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); let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity);
// The mouse position relative to the node // Find the normalized cursor position relative to the node.
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner // (±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. // 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()) { 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 node.node.contains_point(*node.transform, *cursor_position)
&& clip_check_recursive(
if visible_rect *cursor_position,
.normalize(node_rect) *node_entity,
.contains(relative_cursor_position) &clipping_query,
&& pick_rounded_rect( &child_of_query,
*cursor_position - node_rect.center(),
node_rect.size(),
node.node.border_radius,
) )
{ {
hit_nodes hit_nodes
.entry((camera_entity, *pointer_id)) .entry((camera_entity, *pointer_id))
.or_default() .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)); 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
}

View File

@ -2,6 +2,7 @@
use core::{hash::Hash, ops::Range}; use core::{hash::Hash, ops::Range};
use crate::prelude::UiGlobalTransform;
use crate::{ use crate::{
BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystems, BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystems,
ResolvedBorderRadius, TransparentUi, Val, ResolvedBorderRadius, TransparentUi, Val,
@ -18,7 +19,7 @@ use bevy_ecs::{
}, },
}; };
use bevy_image::BevyDefault as _; 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::sync_world::MainEntity;
use bevy_render::RenderApp; use bevy_render::RenderApp;
use bevy_render::{ use bevy_render::{
@ -29,7 +30,6 @@ use bevy_render::{
view::*, view::*,
Extract, ExtractSchedule, Render, RenderSystems, Extract, ExtractSchedule, Render, RenderSystems,
}; };
use bevy_transform::prelude::GlobalTransform;
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
use super::{stack_z_offsets, UiCameraMap, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS}; 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 /// Description of a shadow to be sorted and queued for rendering
pub struct ExtractedBoxShadow { pub struct ExtractedBoxShadow {
pub stack_index: u32, pub stack_index: u32,
pub transform: Mat4, pub transform: Affine2,
pub bounds: Vec2, pub bounds: Vec2,
pub clip: Option<Rect>, pub clip: Option<Rect>,
pub extracted_camera_entity: Entity, pub extracted_camera_entity: Entity,
@ -236,7 +236,7 @@ pub fn extract_shadows(
Query<( Query<(
Entity, Entity,
&ComputedNode, &ComputedNode,
&GlobalTransform, &UiGlobalTransform,
&InheritedVisibility, &InheritedVisibility,
&BoxShadow, &BoxShadow,
Option<&CalculatedClip>, Option<&CalculatedClip>,
@ -302,7 +302,7 @@ pub fn extract_shadows(
extracted_box_shadows.box_shadows.push(ExtractedBoxShadow { extracted_box_shadows.box_shadows.push(ExtractedBoxShadow {
render_entity: commands.spawn(TemporaryRenderEntity).id(), render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index, 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(), color: drop_shadow.color.into(),
bounds: shadow_size + 6. * blur_radius, bounds: shadow_size + 6. * blur_radius,
clip: clip.map(|clip| clip.clip), clip: clip.map(|clip| clip.clip),
@ -405,11 +405,15 @@ pub fn prepare_shadows(
.get(item.index) .get(item.index)
.filter(|n| item.entity() == n.render_entity) .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 // Specify the corners of the node
let positions = QUAD_VERTEX_POSITIONS let positions = QUAD_VERTEX_POSITIONS.map(|pos| {
.map(|pos| (box_shadow.transform * (pos * rect_size).extend(1.)).xyz()); box_shadow
.transform
.transform_point2(pos * rect_size)
.extend(0.)
});
// Calculate the effect of clipping // 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) // 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.), 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 // 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 π // 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(), size: box_shadow.size.into(),
radius, radius,
blur: box_shadow.blur_radius, blur: box_shadow.blur_radius,
bounds: rect_size.xy().into(), bounds: rect_size.into(),
}); });
} }

View File

@ -1,5 +1,6 @@
use crate::shader_flags; use crate::shader_flags;
use crate::ui_node::ComputedNodeTarget; use crate::ui_node::ComputedNodeTarget;
use crate::ui_transform::UiGlobalTransform;
use crate::CalculatedClip; use crate::CalculatedClip;
use crate::ComputedNode; use crate::ComputedNode;
use bevy_asset::AssetId; use bevy_asset::AssetId;
@ -16,7 +17,6 @@ use bevy_render::sync_world::TemporaryRenderEntity;
use bevy_render::view::InheritedVisibility; use bevy_render::view::InheritedVisibility;
use bevy_render::Extract; use bevy_render::Extract;
use bevy_sprite::BorderRect; use bevy_sprite::BorderRect;
use bevy_transform::components::GlobalTransform;
use super::ExtractedUiItem; use super::ExtractedUiItem;
use super::ExtractedUiNode; use super::ExtractedUiNode;
@ -62,9 +62,9 @@ pub fn extract_debug_overlay(
Query<( Query<(
Entity, Entity,
&ComputedNode, &ComputedNode,
&UiGlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
&GlobalTransform,
&ComputedNodeTarget, &ComputedNodeTarget,
)>, )>,
>, >,
@ -76,7 +76,7 @@ pub fn extract_debug_overlay(
let mut camera_mapper = camera_map.get_mapper(); 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() { if !debug_options.show_hidden && !visibility.get() {
continue; continue;
} }
@ -102,7 +102,7 @@ pub fn extract_debug_overlay(
extracted_camera_entity, extracted_camera_entity,
item: ExtractedUiItem::Node { item: ExtractedUiItem::Node {
atlas_scaling: None, atlas_scaling: None,
transform: transform.compute_matrix(), transform: transform.into(),
flip_x: false, flip_x: false,
flip_y: false, flip_y: false,
border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()),

View File

@ -17,8 +17,9 @@ use bevy_ecs::{
use bevy_image::prelude::*; use bevy_image::prelude::*;
use bevy_math::{ use bevy_math::{
ops::{cos, sin}, 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::sync_world::MainEntity;
use bevy_render::{ use bevy_render::{
render_phase::*, render_phase::*,
@ -29,7 +30,6 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderSystems, Extract, ExtractSchedule, Render, RenderSystems,
}; };
use bevy_sprite::BorderRect; use bevy_sprite::BorderRect;
use bevy_transform::prelude::GlobalTransform;
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
use super::shader_flags::BORDER_ALL; use super::shader_flags::BORDER_ALL;
@ -238,7 +238,7 @@ pub enum ResolvedGradient {
pub struct ExtractedGradient { pub struct ExtractedGradient {
pub stack_index: u32, pub stack_index: u32,
pub transform: Mat4, pub transform: Affine2,
pub rect: Rect, pub rect: Rect,
pub clip: Option<Rect>, pub clip: Option<Rect>,
pub extracted_camera_entity: Entity, pub extracted_camera_entity: Entity,
@ -354,7 +354,7 @@ pub fn extract_gradients(
Entity, Entity,
&ComputedNode, &ComputedNode,
&ComputedNodeTarget, &ComputedNodeTarget,
&GlobalTransform, &UiGlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
AnyOf<(&BackgroundGradient, &BorderGradient)>, AnyOf<(&BackgroundGradient, &BorderGradient)>,
@ -414,7 +414,7 @@ pub fn extract_gradients(
border_radius: uinode.border_radius, border_radius: uinode.border_radius,
border: uinode.border, border: uinode.border,
node_type, node_type,
transform: transform.compute_matrix(), transform: transform.into(),
}, },
main_entity: entity.into(), main_entity: entity.into(),
render_entity: commands.spawn(TemporaryRenderEntity).id(), render_entity: commands.spawn(TemporaryRenderEntity).id(),
@ -439,7 +439,7 @@ pub fn extract_gradients(
extracted_gradients.items.push(ExtractedGradient { extracted_gradients.items.push(ExtractedGradient {
render_entity: commands.spawn(TemporaryRenderEntity).id(), render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index, stack_index: uinode.stack_index,
transform: transform.compute_matrix(), transform: transform.into(),
stops_range: range_start..extracted_color_stops.0.len(), stops_range: range_start..extracted_color_stops.0.len(),
rect: Rect { rect: Rect {
min: Vec2::ZERO, min: Vec2::ZERO,
@ -487,7 +487,7 @@ pub fn extract_gradients(
extracted_gradients.items.push(ExtractedGradient { extracted_gradients.items.push(ExtractedGradient {
render_entity: commands.spawn(TemporaryRenderEntity).id(), render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index, stack_index: uinode.stack_index,
transform: transform.compute_matrix(), transform: transform.into(),
stops_range: range_start..extracted_color_stops.0.len(), stops_range: range_start..extracted_color_stops.0.len(),
rect: Rect { rect: Rect {
min: Vec2::ZERO, min: Vec2::ZERO,
@ -541,7 +541,7 @@ pub fn extract_gradients(
extracted_gradients.items.push(ExtractedGradient { extracted_gradients.items.push(ExtractedGradient {
render_entity: commands.spawn(TemporaryRenderEntity).id(), render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index, stack_index: uinode.stack_index,
transform: transform.compute_matrix(), transform: transform.into(),
stops_range: range_start..extracted_color_stops.0.len(), stops_range: range_start..extracted_color_stops.0.len(),
rect: Rect { rect: Rect {
min: Vec2::ZERO, min: Vec2::ZERO,
@ -675,12 +675,16 @@ pub fn prepare_gradient(
*item.batch_range_mut() = item_index as u32..item_index as u32 + 1; *item.batch_range_mut() = item_index as u32..item_index as u32 + 1;
let uinode_rect = gradient.rect; 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 // Specify the corners of the node
let positions = QUAD_VERTEX_POSITIONS let positions = QUAD_VERTEX_POSITIONS.map(|pos| {
.map(|pos| (gradient.transform * (pos * rect_size).extend(1.)).xyz()); gradient
let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); .transform
.transform_point2(pos * rect_size)
.extend(0.)
});
let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size);
// Calculate the effect of clipping // 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) // 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], 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 // 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 π // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π

View File

@ -8,7 +8,9 @@ pub mod ui_texture_slice_pipeline;
mod debug_overlay; mod debug_overlay;
mod gradient; mod gradient;
use crate::prelude::UiGlobalTransform;
use crate::widget::{ImageNode, ViewportNode}; use crate::widget::{ImageNode, ViewportNode};
use crate::{ use crate::{
BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode,
ComputedNodeTarget, Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, 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::prelude::*;
use bevy_ecs::system::SystemParam; use bevy_ecs::system::SystemParam;
use bevy_image::prelude::*; 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::load_shader_library;
use bevy_render::render_graph::{NodeRunError, RenderGraphContext}; use bevy_render::render_graph::{NodeRunError, RenderGraphContext};
use bevy_render::render_phase::ViewSortedRenderPhases; use bevy_render::render_phase::ViewSortedRenderPhases;
@ -243,7 +245,7 @@ pub enum ExtractedUiItem {
/// Ordering: left, top, right, bottom. /// Ordering: left, top, right, bottom.
border: BorderRect, border: BorderRect,
node_type: NodeType, node_type: NodeType,
transform: Mat4, transform: Affine2,
}, },
/// A contiguous sequence of text glyphs from the same section /// A contiguous sequence of text glyphs from the same section
Glyphs { Glyphs {
@ -253,7 +255,7 @@ pub enum ExtractedUiItem {
} }
pub struct ExtractedGlyph { pub struct ExtractedGlyph {
pub transform: Mat4, pub transform: Affine2,
pub rect: Rect, pub rect: Rect,
} }
@ -344,7 +346,7 @@ pub fn extract_uinode_background_colors(
Query<( Query<(
Entity, Entity,
&ComputedNode, &ComputedNode,
&GlobalTransform, &UiGlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
&ComputedNodeTarget, &ComputedNodeTarget,
@ -383,7 +385,7 @@ pub fn extract_uinode_background_colors(
extracted_camera_entity, extracted_camera_entity,
item: ExtractedUiItem::Node { item: ExtractedUiItem::Node {
atlas_scaling: None, atlas_scaling: None,
transform: transform.compute_matrix(), transform: transform.into(),
flip_x: false, flip_x: false,
flip_y: false, flip_y: false,
border: uinode.border(), border: uinode.border(),
@ -403,7 +405,7 @@ pub fn extract_uinode_images(
Query<( Query<(
Entity, Entity,
&ComputedNode, &ComputedNode,
&GlobalTransform, &UiGlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
&ComputedNodeTarget, &ComputedNodeTarget,
@ -467,7 +469,7 @@ pub fn extract_uinode_images(
extracted_camera_entity, extracted_camera_entity,
item: ExtractedUiItem::Node { item: ExtractedUiItem::Node {
atlas_scaling, atlas_scaling,
transform: transform.compute_matrix(), transform: transform.into(),
flip_x: image.flip_x, flip_x: image.flip_x,
flip_y: image.flip_y, flip_y: image.flip_y,
border: uinode.border, border: uinode.border,
@ -487,7 +489,7 @@ pub fn extract_uinode_borders(
Entity, Entity,
&Node, &Node,
&ComputedNode, &ComputedNode,
&GlobalTransform, &UiGlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
&ComputedNodeTarget, &ComputedNodeTarget,
@ -503,7 +505,7 @@ pub fn extract_uinode_borders(
entity, entity,
node, node,
computed_node, computed_node,
global_transform, transform,
inherited_visibility, inherited_visibility,
maybe_clip, maybe_clip,
camera, camera,
@ -567,7 +569,7 @@ pub fn extract_uinode_borders(
extracted_camera_entity, extracted_camera_entity,
item: ExtractedUiItem::Node { item: ExtractedUiItem::Node {
atlas_scaling: None, atlas_scaling: None,
transform: global_transform.compute_matrix(), transform: transform.into(),
flip_x: false, flip_x: false,
flip_y: false, flip_y: false,
border: computed_node.border(), border: computed_node.border(),
@ -600,7 +602,7 @@ pub fn extract_uinode_borders(
clip: maybe_clip.map(|clip| clip.clip), clip: maybe_clip.map(|clip| clip.clip),
extracted_camera_entity, extracted_camera_entity,
item: ExtractedUiItem::Node { item: ExtractedUiItem::Node {
transform: global_transform.compute_matrix(), transform: transform.into(),
atlas_scaling: None, atlas_scaling: None,
flip_x: false, flip_x: false,
flip_y: false, flip_y: false,
@ -749,7 +751,7 @@ pub fn extract_viewport_nodes(
Query<( Query<(
Entity, Entity,
&ComputedNode, &ComputedNode,
&GlobalTransform, &UiGlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
&ComputedNodeTarget, &ComputedNodeTarget,
@ -792,7 +794,7 @@ pub fn extract_viewport_nodes(
extracted_camera_entity, extracted_camera_entity,
item: ExtractedUiItem::Node { item: ExtractedUiItem::Node {
atlas_scaling: None, atlas_scaling: None,
transform: transform.compute_matrix(), transform: transform.into(),
flip_x: false, flip_x: false,
flip_y: false, flip_y: false,
border: uinode.border(), border: uinode.border(),
@ -812,7 +814,7 @@ pub fn extract_text_sections(
Query<( Query<(
Entity, Entity,
&ComputedNode, &ComputedNode,
&GlobalTransform, &UiGlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
&ComputedNodeTarget, &ComputedNodeTarget,
@ -830,7 +832,7 @@ pub fn extract_text_sections(
for ( for (
entity, entity,
uinode, uinode,
global_transform, transform,
inherited_visibility, inherited_visibility,
clip, clip,
camera, camera,
@ -847,8 +849,7 @@ pub fn extract_text_sections(
continue; continue;
}; };
let transform = global_transform.affine() let transform = Affine2::from(*transform) * Affine2::from_translation(-0.5 * uinode.size());
* bevy_math::Affine3A::from_translation((-0.5 * uinode.size()).extend(0.));
for ( for (
i, i,
@ -866,7 +867,7 @@ pub fn extract_text_sections(
.textures[atlas_info.location.glyph_index] .textures[atlas_info.location.glyph_index]
.as_rect(); .as_rect();
extracted_uinodes.glyphs.push(ExtractedGlyph { extracted_uinodes.glyphs.push(ExtractedGlyph {
transform: transform * Mat4::from_translation(position.extend(0.)), transform: transform * Affine2::from_translation(*position),
rect, rect,
}); });
@ -910,8 +911,8 @@ pub fn extract_text_shadows(
Query<( Query<(
Entity, Entity,
&ComputedNode, &ComputedNode,
&UiGlobalTransform,
&ComputedNodeTarget, &ComputedNodeTarget,
&GlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
&TextLayoutInfo, &TextLayoutInfo,
@ -924,16 +925,8 @@ pub fn extract_text_shadows(
let mut end = start + 1; let mut end = start + 1;
let mut camera_mapper = camera_map.get_mapper(); let mut camera_mapper = camera_map.get_mapper();
for ( for (entity, uinode, transform, target, inherited_visibility, clip, text_layout_info, shadow) in
entity, &uinode_query
uinode,
target,
global_transform,
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`) // 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() { if !inherited_visibility.get() || uinode.is_empty() {
@ -944,9 +937,9 @@ pub fn extract_text_shadows(
continue; continue;
}; };
let transform = global_transform.affine() let node_transform = Affine2::from(*transform)
* Mat4::from_translation( * Affine2::from_translation(
(-0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor()).extend(0.), -0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor(),
); );
for ( for (
@ -965,7 +958,7 @@ pub fn extract_text_shadows(
.textures[atlas_info.location.glyph_index] .textures[atlas_info.location.glyph_index]
.as_rect(); .as_rect();
extracted_uinodes.glyphs.push(ExtractedGlyph { extracted_uinodes.glyphs.push(ExtractedGlyph {
transform: transform * Mat4::from_translation(position.extend(0.)), transform: node_transform * Affine2::from_translation(*position),
rect, rect,
}); });
@ -998,7 +991,7 @@ pub fn extract_text_background_colors(
Query<( Query<(
Entity, Entity,
&ComputedNode, &ComputedNode,
&GlobalTransform, &UiGlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
&ComputedNodeTarget, &ComputedNodeTarget,
@ -1021,8 +1014,8 @@ pub fn extract_text_background_colors(
continue; continue;
}; };
let transform = global_transform.affine() let transform =
* bevy_math::Affine3A::from_translation(-0.5 * uinode.size().extend(0.)); Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size());
for &(section_entity, rect) in text_layout_info.section_rects.iter() { for &(section_entity, rect) in text_layout_info.section_rects.iter() {
let Ok(text_background_color) = text_background_colors_query.get(section_entity) else { 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, extracted_camera_entity,
item: ExtractedUiItem::Node { item: ExtractedUiItem::Node {
atlas_scaling: None, atlas_scaling: None,
transform: transform * Mat4::from_translation(rect.center().extend(0.)), transform: transform * Affine2::from_translation(rect.center()),
flip_x: false, flip_x: false,
flip_y: false, flip_y: false,
border: uinode.border(), border: uinode.border(),
@ -1093,11 +1086,11 @@ impl Default for UiMeta {
} }
} }
pub(crate) const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ pub(crate) const QUAD_VERTEX_POSITIONS: [Vec2; 4] = [
Vec3::new(-0.5, -0.5, 0.0), Vec2::new(-0.5, -0.5),
Vec3::new(0.5, -0.5, 0.0), Vec2::new(0.5, -0.5),
Vec3::new(0.5, 0.5, 0.0), Vec2::new(0.5, 0.5),
Vec3::new(-0.5, 0.5, 0.0), Vec2::new(-0.5, 0.5),
]; ];
pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; 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 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 // Specify the corners of the node
let positions = QUAD_VERTEX_POSITIONS let positions = QUAD_VERTEX_POSITIONS
.map(|pos| (*transform * (pos * rect_size).extend(1.)).xyz()); .map(|pos| transform.transform_point2(pos * rect_size).extend(0.));
let points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); let points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size);
// Calculate the effect of clipping // 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) // 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], 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 // 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 π // 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_radius.bottom_left,
], ],
border: [border.left, border.top, border.right, border.bottom], border: [border.left, border.top, border.right, border.bottom],
size: rect_size.xy().into(), size: rect_size.into(),
point: points[i].into(), point: points[i].into(),
}); });
} }
@ -1470,13 +1463,14 @@ pub fn prepare_uinodes(
let color = extracted_uinode.color.to_f32_array(); let color = extracted_uinode.color.to_f32_array();
for glyph in &extracted_uinodes.glyphs[range.clone()] { for glyph in &extracted_uinodes.glyphs[range.clone()] {
let glyph_rect = glyph.rect; let glyph_rect = glyph.rect;
let size = glyph.rect.size(); let rect_size = glyph_rect.size();
let rect_size = glyph_rect.size().extend(1.0);
// Specify the corners of the glyph // Specify the corners of the glyph
let positions = QUAD_VERTEX_POSITIONS.map(|pos| { 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 { let positions_diff = if let Some(clip) = extracted_uinode.clip {
@ -1511,7 +1505,7 @@ pub fn prepare_uinodes(
// cull nodes that are completely clipped // cull nodes that are completely clipped
let transformed_rect_size = 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 if positions_diff[0].x - positions_diff[1].x
>= transformed_rect_size.x.abs() >= transformed_rect_size.x.abs()
|| positions_diff[1].y - positions_diff[2].y || positions_diff[1].y - positions_diff[2].y
@ -1548,7 +1542,7 @@ pub fn prepare_uinodes(
flags: shader_flags::TEXTURED | shader_flags::CORNERS[i], flags: shader_flags::TEXTURED | shader_flags::CORNERS[i],
radius: [0.0; 4], radius: [0.0; 4],
border: [0.0; 4], border: [0.0; 4],
size: size.into(), size: rect_size.into(),
point: [0.0; 2], point: [0.0; 2],
}); });
} }

View File

@ -1,9 +1,7 @@
use core::{hash::Hash, marker::PhantomData, ops::Range};
use crate::*; use crate::*;
use bevy_asset::*; use bevy_asset::*;
use bevy_ecs::{ use bevy_ecs::{
prelude::Component, prelude::{Component, With},
query::ROQueryItem, query::ROQueryItem,
system::{ system::{
lifetimeless::{Read, SRes}, lifetimeless::{Read, SRes},
@ -11,24 +9,22 @@ use bevy_ecs::{
}, },
}; };
use bevy_image::BevyDefault as _; use bevy_image::BevyDefault as _;
use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; use bevy_math::{Affine2, FloatOrd, Rect, Vec2};
use bevy_render::{ use bevy_render::{
extract_component::ExtractComponentPlugin, extract_component::ExtractComponentPlugin,
globals::{GlobalsBuffer, GlobalsUniform}, globals::{GlobalsBuffer, GlobalsUniform},
load_shader_library,
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets},
render_phase::*, render_phase::*,
render_resource::{binding_types::uniform_buffer, *}, render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderDevice, RenderQueue}, renderer::{RenderDevice, RenderQueue},
sync_world::{MainEntity, TemporaryRenderEntity},
view::*, view::*,
Extract, ExtractSchedule, Render, RenderSystems, Extract, ExtractSchedule, Render, RenderSystems,
}; };
use bevy_render::{
load_shader_library,
sync_world::{MainEntity, TemporaryRenderEntity},
};
use bevy_sprite::BorderRect; use bevy_sprite::BorderRect;
use bevy_transform::prelude::GlobalTransform;
use bytemuck::{Pod, Zeroable}; 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 /// Adds the necessary ECS resources and render logic to enable rendering entities using the given
/// [`UiMaterial`] asset type (which includes [`UiMaterial`] types). /// [`UiMaterial`] asset type (which includes [`UiMaterial`] types).
@ -321,7 +317,7 @@ impl<P: PhaseItem, M: UiMaterial> RenderCommand<P> for DrawUiMaterialNode<M> {
pub struct ExtractedUiMaterialNode<M: UiMaterial> { pub struct ExtractedUiMaterialNode<M: UiMaterial> {
pub stack_index: u32, pub stack_index: u32,
pub transform: Mat4, pub transform: Affine2,
pub rect: Rect, pub rect: Rect,
pub border: BorderRect, pub border: BorderRect,
pub border_radius: ResolvedBorderRadius, pub border_radius: ResolvedBorderRadius,
@ -356,7 +352,7 @@ pub fn extract_ui_material_nodes<M: UiMaterial>(
Query<( Query<(
Entity, Entity,
&ComputedNode, &ComputedNode,
&GlobalTransform, &UiGlobalTransform,
&MaterialNode<M>, &MaterialNode<M>,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
@ -387,7 +383,7 @@ pub fn extract_ui_material_nodes<M: UiMaterial>(
extracted_uinodes.uinodes.push(ExtractedUiMaterialNode { extracted_uinodes.uinodes.push(ExtractedUiMaterialNode {
render_entity: commands.spawn(TemporaryRenderEntity).id(), render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: computed_node.stack_index, stack_index: computed_node.stack_index,
transform: transform.compute_matrix(), transform: transform.into(),
material: handle.id(), material: handle.id(),
rect: Rect { rect: Rect {
min: Vec2::ZERO, min: Vec2::ZERO,
@ -459,10 +455,13 @@ pub fn prepare_uimaterial_nodes<M: UiMaterial>(
let uinode_rect = extracted_uinode.rect; 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| { 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 { let positions_diff = if let Some(clip) = extracted_uinode.clip {
@ -496,7 +495,7 @@ pub fn prepare_uimaterial_nodes<M: UiMaterial>(
]; ];
let transformed_rect_size = 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 // 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 π // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π

View File

@ -1,5 +1,6 @@
use core::{hash::Hash, ops::Range}; use core::{hash::Hash, ops::Range};
use crate::prelude::UiGlobalTransform;
use crate::*; use crate::*;
use bevy_asset::*; use bevy_asset::*;
use bevy_color::{Alpha, ColorToComponents, LinearRgba}; use bevy_color::{Alpha, ColorToComponents, LinearRgba};
@ -11,7 +12,7 @@ use bevy_ecs::{
}, },
}; };
use bevy_image::prelude::*; 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_platform::collections::HashMap;
use bevy_render::sync_world::MainEntity; use bevy_render::sync_world::MainEntity;
use bevy_render::{ use bevy_render::{
@ -25,7 +26,6 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderSystems, Extract, ExtractSchedule, Render, RenderSystems,
}; };
use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer}; use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer};
use bevy_transform::prelude::GlobalTransform;
use binding_types::{sampler, texture_2d}; use binding_types::{sampler, texture_2d};
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
use widget::ImageNode; use widget::ImageNode;
@ -218,7 +218,7 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline {
pub struct ExtractedUiTextureSlice { pub struct ExtractedUiTextureSlice {
pub stack_index: u32, pub stack_index: u32,
pub transform: Mat4, pub transform: Affine2,
pub rect: Rect, pub rect: Rect,
pub atlas_rect: Option<Rect>, pub atlas_rect: Option<Rect>,
pub image: AssetId<Image>, pub image: AssetId<Image>,
@ -246,7 +246,7 @@ pub fn extract_ui_texture_slices(
Query<( Query<(
Entity, Entity,
&ComputedNode, &ComputedNode,
&GlobalTransform, &UiGlobalTransform,
&InheritedVisibility, &InheritedVisibility,
Option<&CalculatedClip>, Option<&CalculatedClip>,
&ComputedNodeTarget, &ComputedNodeTarget,
@ -306,7 +306,7 @@ pub fn extract_ui_texture_slices(
extracted_ui_slicers.slices.push(ExtractedUiTextureSlice { extracted_ui_slicers.slices.push(ExtractedUiTextureSlice {
render_entity: commands.spawn(TemporaryRenderEntity).id(), render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index, stack_index: uinode.stack_index,
transform: transform.compute_matrix(), transform: transform.into(),
color: image.color.into(), color: image.color.into(),
rect: Rect { rect: Rect {
min: Vec2::ZERO, min: Vec2::ZERO,
@ -497,11 +497,12 @@ pub fn prepare_ui_slices(
let uinode_rect = texture_slices.rect; 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 // Specify the corners of the node
let positions = QUAD_VERTEX_POSITIONS let positions = QUAD_VERTEX_POSITIONS.map(|pos| {
.map(|pos| (texture_slices.transform * (pos * rect_size).extend(1.)).xyz()); (texture_slices.transform.transform_point2(pos * rect_size)).extend(0.)
});
// Calculate the effect of clipping // 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) // 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 = 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 // 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 π // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π

View File

@ -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_color::{Alpha, Color};
use bevy_derive::{Deref, DerefMut}; use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_ecs::{prelude::*, system::SystemParam};
@ -9,7 +12,6 @@ use bevy_render::{
view::Visibility, view::Visibility,
}; };
use bevy_sprite::BorderRect; use bevy_sprite::BorderRect;
use bevy_transform::components::Transform;
use bevy_utils::once; use bevy_utils::once;
use bevy_window::{PrimaryWindow, WindowRef}; use bevy_window::{PrimaryWindow, WindowRef};
use core::{f32, num::NonZero}; use core::{f32, num::NonZero};
@ -229,6 +231,73 @@ impl ComputedNode {
pub const fn inverse_scale_factor(&self) -> f32 { pub const fn inverse_scale_factor(&self) -> f32 {
self.inverse_scale_factor 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<Vec2> {
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 { impl ComputedNode {
@ -323,12 +392,12 @@ impl From<Vec2> for ScrollPosition {
#[require( #[require(
ComputedNode, ComputedNode,
ComputedNodeTarget, ComputedNodeTarget,
UiTransform,
BackgroundColor, BackgroundColor,
BorderColor, BorderColor,
BorderRadius, BorderRadius,
FocusPolicy, FocusPolicy,
ScrollPosition, ScrollPosition,
Transform,
Visibility, Visibility,
ZIndex ZIndex
)] )]

View File

@ -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<Affine2> {
(self.matrix2.determinant() != 0.).then_some(self.inverse())
}
}
impl From<Affine2> for UiGlobalTransform {
fn from(value: Affine2) -> Self {
Self(value)
}
}
impl From<UiGlobalTransform> for Affine2 {
fn from(value: UiGlobalTransform) -> Self {
value.0
}
}
impl From<&UiGlobalTransform> for Affine2 {
fn from(value: &UiGlobalTransform) -> Self {
value.0
}
}

View File

@ -2,6 +2,7 @@
use crate::{ use crate::{
experimental::{UiChildren, UiRootNodes}, experimental::{UiChildren, UiRootNodes},
ui_transform::UiGlobalTransform,
CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale, CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale,
UiTargetCamera, UiTargetCamera,
}; };
@ -17,7 +18,6 @@ use bevy_ecs::{
use bevy_math::{Rect, UVec2}; use bevy_math::{Rect, UVec2};
use bevy_render::camera::Camera; use bevy_render::camera::Camera;
use bevy_sprite::BorderRect; use bevy_sprite::BorderRect;
use bevy_transform::components::GlobalTransform;
/// Updates clipping for all nodes /// Updates clipping for all nodes
pub fn update_clipping_system( pub fn update_clipping_system(
@ -26,7 +26,7 @@ pub fn update_clipping_system(
mut node_query: Query<( mut node_query: Query<(
&Node, &Node,
&ComputedNode, &ComputedNode,
&GlobalTransform, &UiGlobalTransform,
Option<&mut CalculatedClip>, Option<&mut CalculatedClip>,
)>, )>,
ui_children: UiChildren, ui_children: UiChildren,
@ -48,14 +48,13 @@ fn update_clipping(
node_query: &mut Query<( node_query: &mut Query<(
&Node, &Node,
&ComputedNode, &ComputedNode,
&GlobalTransform, &UiGlobalTransform,
Option<&mut CalculatedClip>, Option<&mut CalculatedClip>,
)>, )>,
entity: Entity, entity: Entity,
mut maybe_inherited_clip: Option<Rect>, mut maybe_inherited_clip: Option<Rect>,
) { ) {
let Ok((node, computed_node, global_transform, maybe_calculated_clip)) = let Ok((node, computed_node, transform, maybe_calculated_clip)) = node_query.get_mut(entity)
node_query.get_mut(entity)
else { else {
return; return;
}; };
@ -91,10 +90,7 @@ fn update_clipping(
maybe_inherited_clip maybe_inherited_clip
} else { } else {
// Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists // 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( let mut clip_rect = Rect::from_center_size(transform.translation, computed_node.size());
global_transform.translation().truncate(),
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`]. // Content isn't clipped at the edges of the node but at the edges of the region specified by [`Node::overflow_clip_margin`].
// //

View File

@ -171,11 +171,6 @@ pub fn update_viewport_render_target_size(
height: u32::max(1, size.y as u32), height: u32::max(1, size.y as u32),
..default() ..default()
}; };
let image = images.get_mut(image_handle).unwrap(); images.get_mut(image_handle).unwrap().resize(size);
if image.data.is_some() {
image.resize(size);
} else {
image.texture_descriptor.size = size;
}
} }
} }

View File

@ -194,7 +194,7 @@ fn update_cursors(
fn on_remove_cursor_icon(trigger: Trigger<OnRemove, CursorIcon>, mut commands: Commands) { fn on_remove_cursor_icon(trigger: Trigger<OnRemove, CursorIcon>, mut commands: Commands) {
// Use `try_insert` to avoid panic if the window is being destroyed. // Use `try_insert` to avoid panic if the window is being destroyed.
commands commands
.entity(trigger.target()) .entity(trigger.target().unwrap())
.try_insert(PendingCursor(Some(CursorSource::System( .try_insert(PendingCursor(Some(CursorSource::System(
convert_system_cursor_icon(SystemCursorIcon::Default), convert_system_cursor_icon(SystemCursorIcon::Default),
)))); ))));

View File

@ -65,12 +65,12 @@ fn change_material(
mut asset_materials: ResMut<Assets<StandardMaterial>>, mut asset_materials: ResMut<Assets<StandardMaterial>>,
) { ) {
// Get the `ColorOverride` of the entity, if it does not have a color override, skip // 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; return;
}; };
// Iterate over all children recursively // 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 // Get the material of the descendant
if let Some(material) = mesh_materials if let Some(material) = mesh_materials
.get(descendants) .get(descendants)

View File

@ -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 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](../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 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 [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 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 [Viewport Node](../examples/ui/viewport_node.rs) | Demonstrates how to create a viewport node with picking support

View File

@ -70,12 +70,12 @@ fn play_animation_when_ready(
) { ) {
// The entity we spawned in `setup_mesh_and_animation` is the trigger's target. // 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. // 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 // The SceneRoot component will have spawned the scene as a hierarchy
// of entities parented to our entity. Since the asset contained a skinned // of entities parented to our entity. Since the asset contained a skinned
// mesh and animations, it will also have spawned an animation player // mesh and animations, it will also have spawned an animation player
// component. Search our entity's descendants to find the 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) { if let Ok(mut player) = players.get_mut(child) {
// Tell the animation player to start the animation and keep // Tell the animation player to start the animation and keep
// repeating it. // repeating it.

View File

@ -47,7 +47,10 @@ fn observe_on_step(
transforms: Query<&GlobalTransform>, transforms: Query<&GlobalTransform>,
mut seeded_rng: ResMut<SeededRng>, mut seeded_rng: ResMut<SeededRng>,
) { ) {
let translation = transforms.get(trigger.target()).unwrap().translation(); let translation = transforms
.get(trigger.target().unwrap())
.unwrap()
.translation();
// Spawn a bunch of particles. // Spawn a bunch of particles.
for _ in 0..14 { for _ in 0..14 {
let horizontal = seeded_rng.0.r#gen::<Dir2>() * seeded_rng.0.gen_range(8.0..12.0); let horizontal = seeded_rng.0.r#gen::<Dir2>() * seeded_rng.0.gen_range(8.0..12.0);

View File

@ -40,7 +40,7 @@ fn disable_entities_on_click(
valid_query: Query<&DisableOnClick>, valid_query: Query<&DisableOnClick>,
mut commands: Commands, mut commands: Commands,
) { ) {
let clicked_entity = trigger.target(); let clicked_entity = trigger.target().unwrap();
// Windows and text are entities and can be clicked! // Windows and text are entities and can be clicked!
// We definitely don't want to disable the window itself, // We definitely don't want to disable the window itself,
// because that would cause the app to close! // because that would cause the app to close!

View File

@ -78,14 +78,14 @@ fn attack_armor(entities: Query<Entity, With<Armor>>, mut commands: Commands) {
} }
fn attack_hits(trigger: Trigger<Attack>, name: Query<&Name>) { fn attack_hits(trigger: Trigger<Attack>, name: Query<&Name>) {
if let Ok(name) = name.get(trigger.target()) { if let Ok(name) = name.get(trigger.target().unwrap()) {
info!("Attack hit {}", name); info!("Attack hit {}", name);
} }
} }
/// A callback placed on [`Armor`], checking if it absorbed all the [`Attack`] damage. /// A callback placed on [`Armor`], checking if it absorbed all the [`Attack`] damage.
fn block_attack(mut trigger: Trigger<Attack>, armor: Query<(&Armor, &Name)>) { fn block_attack(mut trigger: Trigger<Attack>, 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 attack = trigger.event_mut();
let damage = attack.damage.saturating_sub(**armor); let damage = attack.damage.saturating_sub(**armor);
if damage > 0 { if damage > 0 {
@ -110,14 +110,14 @@ fn take_damage(
mut app_exit: EventWriter<AppExit>, mut app_exit: EventWriter<AppExit>,
) { ) {
let attack = trigger.event(); 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); **hp = hp.saturating_sub(attack.damage);
if **hp > 0 { if **hp > 0 {
info!("{} has {:.1} HP", name, hp.0); info!("{} has {:.1} HP", name, hp.0);
} else { } else {
warn!("💀 {} has died a gruesome death", name); warn!("💀 {} has died a gruesome death", name);
commands.entity(trigger.target()).despawn(); commands.entity(trigger.target().unwrap()).despawn();
app_exit.write(AppExit::Success); app_exit.write(AppExit::Success);
} }

View File

@ -117,12 +117,16 @@ fn on_add_mine(
query: Query<&Mine>, query: Query<&Mine>,
mut index: ResMut<SpatialIndex>, mut index: ResMut<SpatialIndex>,
) { ) {
let mine = query.get(trigger.target()).unwrap(); let mine = query.get(trigger.target().unwrap()).unwrap();
let tile = ( let tile = (
(mine.pos.x / CELL_SIZE).floor() as i32, (mine.pos.x / CELL_SIZE).floor() as i32,
(mine.pos.y / 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 // Remove despawned mines from our index
@ -131,19 +135,19 @@ fn on_remove_mine(
query: Query<&Mine>, query: Query<&Mine>,
mut index: ResMut<SpatialIndex>, mut index: ResMut<SpatialIndex>,
) { ) {
let mine = query.get(trigger.target()).unwrap(); let mine = query.get(trigger.target().unwrap()).unwrap();
let tile = ( let tile = (
(mine.pos.x / CELL_SIZE).floor() as i32, (mine.pos.x / CELL_SIZE).floor() as i32,
(mine.pos.y / CELL_SIZE).floor() as i32, (mine.pos.y / CELL_SIZE).floor() as i32,
); );
index.map.entry(tile).and_modify(|set| { index.map.entry(tile).and_modify(|set| {
set.remove(&trigger.target()); set.remove(&trigger.target().unwrap());
}); });
} }
fn explode_mine(trigger: Trigger<Explode>, query: Query<&Mine>, mut commands: Commands) { fn explode_mine(trigger: Trigger<Explode>, query: Query<&Mine>, mut commands: Commands) {
// If a triggered event is targeting a specific entity you can access it with `.target()` // 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 { let Ok(mut entity) = commands.get_entity(id) else {
return; return;
}; };

View File

@ -50,7 +50,7 @@ fn remove_component(
fn react_on_removal(trigger: Trigger<OnRemove, MyComponent>, mut query: Query<&mut Sprite>) { fn react_on_removal(trigger: Trigger<OnRemove, MyComponent>, mut query: Query<&mut Sprite>) {
// The `OnRemove` trigger was automatically called on the `Entity` that had its `MyComponent` removed. // 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) { if let Ok(mut sprite) = query.get_mut(entity) {
sprite.color = Color::srgb(0.5, 1., 1.); sprite.color = Color::srgb(0.5, 1., 1.);
} }

View File

@ -127,7 +127,10 @@ fn tick_timers(
} }
fn unwrap<B: Bundle>(trigger: Trigger<Unwrap>, world: &mut World) { fn unwrap<B: Bundle>(trigger: Trigger<Unwrap>, 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::<DelayedComponent<B>>() { if let Some(DelayedComponent(bundle)) = target.take::<DelayedComponent<B>>() {
target.insert(bundle); target.insert(bundle);
} }

View File

@ -48,13 +48,13 @@ fn setup_scene(
.observe(on_click_spawn_cube) .observe(on_click_spawn_cube)
.observe( .observe(
|out: Trigger<Pointer<Out>>, mut texts: Query<&mut TextColor>| { |out: Trigger<Pointer<Out>>, 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; text_color.0 = Color::WHITE;
}, },
) )
.observe( .observe(
|over: Trigger<Pointer<Over>>, mut texts: Query<&mut TextColor>| { |over: Trigger<Pointer<Over>>, 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(); color.0 = bevy::color::palettes::tailwind::CYAN_400.into();
}, },
); );
@ -102,7 +102,7 @@ fn on_click_spawn_cube(
} }
fn on_drag_rotate(drag: Trigger<Pointer<Drag>>, mut transforms: Query<&mut Transform>) { fn on_drag_rotate(drag: Trigger<Pointer<Drag>>, 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_y(drag.delta.x * 0.02);
transform.rotate_x(drag.delta.y * 0.02); transform.rotate_x(drag.delta.y * 0.02);
} }

View File

@ -164,7 +164,7 @@ fn update_material_on<E>(
// versions of this observer, each triggered by a different event and with a different hardcoded // 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. // material. Instead, the event type is a generic, and the material is passed in.
move |trigger, mut query| { 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(); material.0 = new_material.clone();
} }
} }
@ -191,7 +191,7 @@ fn rotate(mut query: Query<&mut Transform, With<Shape>>, time: Res<Time>) {
/// An observer to rotate an entity when it is dragged /// An observer to rotate an entity when it is dragged
fn rotate_on_drag(drag: Trigger<Pointer<Drag>>, mut transforms: Query<&mut Transform>) { fn rotate_on_drag(drag: Trigger<Pointer<Drag>>, mut transforms: Query<&mut Transform>) {
let mut transform = transforms.get_mut(drag.target()).unwrap(); let mut transform = transforms.get_mut(drag.target().unwrap()).unwrap();
transform.rotate_y(drag.delta.x * 0.02); transform.rotate_y(drag.delta.x * 0.02);
transform.rotate_x(drag.delta.y * 0.02); transform.rotate_x(drag.delta.y * 0.02);
} }

View File

@ -27,13 +27,13 @@ fn setup_scene(
.observe(on_click_spawn_cube) .observe(on_click_spawn_cube)
.observe( .observe(
|out: Trigger<Pointer<Out>>, mut texts: Query<&mut TextColor>| { |out: Trigger<Pointer<Out>>, 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; text_color.0 = Color::WHITE;
}, },
) )
.observe( .observe(
|over: Trigger<Pointer<Over>>, mut texts: Query<&mut TextColor>| { |over: Trigger<Pointer<Over>>, 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(); color.0 = bevy::color::palettes::tailwind::CYAN_400.into();
}, },
); );
@ -80,7 +80,7 @@ fn on_click_spawn_cube(
} }
fn on_drag_rotate(drag: Trigger<Pointer<Drag>>, mut transforms: Query<&mut Transform>) { fn on_drag_rotate(drag: Trigger<Pointer<Drag>>, 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_y(drag.delta.x * 0.02);
transform.rotate_x(drag.delta.y * 0.02); transform.rotate_x(drag.delta.y * 0.02);
} }

View File

@ -152,7 +152,7 @@ fn setup_atlas(
// An observer listener that changes the target entity's color. // An observer listener that changes the target entity's color.
fn recolor_on<E: Debug + Clone + Reflect>(color: Color) -> impl Fn(Trigger<E>, Query<&mut Sprite>) { fn recolor_on<E: Debug + Clone + Reflect>(color: Color) -> impl Fn(Trigger<E>, Query<&mut Sprite>) {
move |ev, mut sprites| { move |ev, mut sprites| {
let Ok(mut sprite) = sprites.get_mut(ev.target()) else { let Ok(mut sprite) = sprites.get_mut(ev.target().unwrap()) else {
return; return;
}; };
sprite.color = color; sprite.color = color;

View File

@ -281,7 +281,7 @@ mod animation {
animation: Res<Animation>, animation: Res<Animation>,
mut players: Query<(Entity, &mut AnimationPlayer)>, mut players: Query<(Entity, &mut AnimationPlayer)>,
) { ) {
for child in children.iter_descendants(trigger.target()) { for child in children.iter_descendants(trigger.target().unwrap()) {
if let Ok((entity, mut player)) = players.get_mut(child) { if let Ok((entity, mut player)) = players.get_mut(child) {
let mut transitions = AnimationTransitions::new(); let mut transitions = AnimationTransitions::new();
transitions transitions

View File

@ -228,7 +228,13 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
parent.spawn(( parent.spawn((
ImageNode::new(asset_server.load("branding/bevy_logo_light.png")), ImageNode::new(asset_server.load("branding/bevy_logo_light.png")),
// Uses the transform to rotate the logo image by 45 degrees // Uses the transform to rotate the logo image by 45 degrees
Transform::from_rotation(Quat::from_rotation_z(0.25 * PI)), Node {
..Default::default()
},
UiTransform {
rotation: Rot2::radians(0.25 * PI),
..Default::default()
},
BorderRadius::all(Val::Px(10.)), BorderRadius::all(Val::Px(10.)),
Outline { Outline {
width: Val::Px(2.), width: Val::Px(2.),

View File

@ -70,7 +70,7 @@ fn universal_button_click_behavior(
mut trigger: Trigger<Pointer<Click>>, mut trigger: Trigger<Pointer<Click>>,
mut button_query: Query<(&mut BackgroundColor, &mut ResetTimer)>, mut button_query: Query<(&mut BackgroundColor, &mut ResetTimer)>,
) { ) {
let button_entity = trigger.target(); let button_entity = trigger.target().unwrap();
if let Ok((mut color, mut reset_timer)) = button_query.get_mut(button_entity) { if let Ok((mut color, mut reset_timer)) = button_query.get_mut(button_entity) {
// This would be a great place to play a little sound effect too! // This would be a great place to play a little sound effect too!
color.0 = PRESSED_BUTTON.into(); color.0 = PRESSED_BUTTON.into();

View File

@ -26,20 +26,20 @@ fn setup(mut commands: Commands) {
commands commands
.spawn(Node { .spawn(Node {
flex_direction: FlexDirection::Column, flex_direction: FlexDirection::Column,
row_gap: Val::Px(30.), row_gap: Val::Px(20.),
margin: UiRect::all(Val::Px(30.)), margin: UiRect::all(Val::Px(20.)),
..Default::default() ..Default::default()
}) })
.with_children(|commands| { .with_children(|commands| {
for (b, stops) in [ for (b, stops) in [
( (
5., 4.,
vec![ vec![
ColorStop::new(Color::WHITE, Val::Percent(15.)), ColorStop::new(Color::WHITE, Val::Percent(15.)),
ColorStop::new(Color::BLACK, Val::Percent(85.)), ColorStop::new(Color::BLACK, Val::Percent(85.)),
], ],
), ),
(5., vec![RED.into(), BLUE.into(), LIME.into()]), (4., vec![RED.into(), BLUE.into(), LIME.into()]),
( (
0., 0.,
vec![ vec![
@ -64,11 +64,11 @@ fn setup(mut commands: Commands) {
commands commands
.spawn(Node { .spawn(Node {
flex_direction: FlexDirection::Column, flex_direction: FlexDirection::Column,
row_gap: Val::Px(10.), row_gap: Val::Px(5.),
..Default::default() ..Default::default()
}) })
.with_children(|commands| { .with_children(|commands| {
for (w, h) in [(100., 100.), (50., 100.), (100., 50.)] { for (w, h) in [(70., 70.), (35., 70.), (70., 35.)] {
commands commands
.spawn(Node { .spawn(Node {
column_gap: Val::Px(10.), column_gap: Val::Px(10.),
@ -108,7 +108,7 @@ fn setup(mut commands: Commands) {
aspect_ratio: Some(1.), aspect_ratio: Some(1.),
height: Val::Percent(100.), height: Val::Percent(100.),
border: UiRect::all(Val::Px(b)), border: UiRect::all(Val::Px(b)),
margin: UiRect::left(Val::Px(30.)), margin: UiRect::left(Val::Px(20.)),
..default() ..default()
}, },
BorderRadius::all(Val::Px(20.)), BorderRadius::all(Val::Px(20.)),
@ -128,7 +128,7 @@ fn setup(mut commands: Commands) {
aspect_ratio: Some(1.), aspect_ratio: Some(1.),
height: Val::Percent(100.), height: Val::Percent(100.),
border: UiRect::all(Val::Px(b)), border: UiRect::all(Val::Px(b)),
margin: UiRect::left(Val::Px(30.)), margin: UiRect::left(Val::Px(20.)),
..default() ..default()
}, },
BorderRadius::all(Val::Px(20.)), BorderRadius::all(Val::Px(20.)),
@ -148,7 +148,7 @@ fn setup(mut commands: Commands) {
aspect_ratio: Some(1.), aspect_ratio: Some(1.),
height: Val::Percent(100.), height: Val::Percent(100.),
border: UiRect::all(Val::Px(b)), border: UiRect::all(Val::Px(b)),
margin: UiRect::left(Val::Px(30.)), margin: UiRect::left(Val::Px(20.)),
..default() ..default()
}, },
BorderRadius::all(Val::Px(20.)), BorderRadius::all(Val::Px(20.)),

View File

@ -4,7 +4,6 @@ use bevy::{input::common_conditions::input_just_pressed, prelude::*, ui::widget:
use std::f32::consts::{FRAC_PI_2, PI, TAU}; use std::f32::consts::{FRAC_PI_2, PI, TAU};
const CONTAINER_SIZE: f32 = 150.0; const CONTAINER_SIZE: f32 = 150.0;
const HALF_CONTAINER_SIZE: f32 = CONTAINER_SIZE / 2.0;
const LOOP_LENGTH: f32 = 4.0; const LOOP_LENGTH: f32 = 4.0;
fn main() { fn main() {
@ -41,16 +40,16 @@ struct AnimationState {
struct Container(u8); struct Container(u8);
trait UpdateTransform { trait UpdateTransform {
fn update(&self, t: f32, transform: &mut Transform); fn update(&self, t: f32, transform: &mut UiTransform);
} }
#[derive(Component)] #[derive(Component)]
struct Move; struct Move;
impl UpdateTransform for Move { impl UpdateTransform for Move {
fn update(&self, t: f32, transform: &mut Transform) { fn update(&self, t: f32, transform: &mut UiTransform) {
transform.translation.x = ops::sin(t * TAU - FRAC_PI_2) * HALF_CONTAINER_SIZE; transform.translation.x = Val::Percent(ops::sin(t * TAU - FRAC_PI_2) * 50.);
transform.translation.y = -ops::cos(t * TAU - FRAC_PI_2) * HALF_CONTAINER_SIZE; transform.translation.y = Val::Percent(-ops::cos(t * TAU - FRAC_PI_2) * 50.);
} }
} }
@ -58,7 +57,7 @@ impl UpdateTransform for Move {
struct Scale; struct Scale;
impl UpdateTransform for Scale { impl UpdateTransform for Scale {
fn update(&self, t: f32, transform: &mut Transform) { fn update(&self, t: f32, transform: &mut UiTransform) {
transform.scale.x = 1.0 + 0.5 * ops::cos(t * TAU).max(0.0); transform.scale.x = 1.0 + 0.5 * ops::cos(t * TAU).max(0.0);
transform.scale.y = 1.0 + 0.5 * ops::cos(t * TAU + PI).max(0.0); transform.scale.y = 1.0 + 0.5 * ops::cos(t * TAU + PI).max(0.0);
} }
@ -68,9 +67,8 @@ impl UpdateTransform for Scale {
struct Rotate; struct Rotate;
impl UpdateTransform for Rotate { impl UpdateTransform for Rotate {
fn update(&self, t: f32, transform: &mut Transform) { fn update(&self, t: f32, transform: &mut UiTransform) {
transform.rotation = transform.rotation = Rot2::radians(ops::cos(t * TAU) * 45.0);
Quat::from_axis_angle(Vec3::Z, (ops::cos(t * TAU) * 45.0).to_radians());
} }
} }
@ -175,10 +173,6 @@ fn spawn_container(
update_transform: impl UpdateTransform + Component, update_transform: impl UpdateTransform + Component,
spawn_children: impl FnOnce(&mut ChildSpawnerCommands), spawn_children: impl FnOnce(&mut ChildSpawnerCommands),
) { ) {
let mut transform = Transform::default();
update_transform.update(0.0, &mut transform);
parent parent
.spawn(( .spawn((
Node { Node {
@ -198,11 +192,8 @@ fn spawn_container(
Node { Node {
align_items: AlignItems::Center, align_items: AlignItems::Center,
justify_content: JustifyContent::Center, justify_content: JustifyContent::Center,
top: Val::Px(transform.translation.x),
left: Val::Px(transform.translation.y),
..default() ..default()
}, },
transform,
update_transform, update_transform,
)) ))
.with_children(spawn_children); .with_children(spawn_children);
@ -233,13 +224,10 @@ fn update_animation(
fn update_transform<T: UpdateTransform + Component>( fn update_transform<T: UpdateTransform + Component>(
animation: Res<AnimationState>, animation: Res<AnimationState>,
mut containers: Query<(&mut Transform, &mut Node, &ComputedNode, &T)>, mut containers: Query<(&mut UiTransform, &T)>,
) { ) {
for (mut transform, mut node, computed_node, update_transform) in &mut containers { for (mut transform, update_transform) in &mut containers {
update_transform.update(animation.t, &mut transform); update_transform.update(animation.t, &mut transform);
node.left = Val::Px(transform.translation.x * computed_node.inverse_scale_factor());
node.top = Val::Px(transform.translation.y * computed_node.inverse_scale_factor());
} }
} }

View File

@ -78,7 +78,7 @@ fn relative_cursor_position_system(
"unknown".to_string() "unknown".to_string()
}; };
text_color.0 = if relative_cursor_position.mouse_over() { text_color.0 = if relative_cursor_position.cursor_over() {
Color::srgb(0.1, 0.9, 0.1) Color::srgb(0.1, 0.9, 0.1)
} else { } else {
Color::srgb(0.9, 0.1, 0.1) Color::srgb(0.9, 0.1, 0.1)

View File

@ -93,7 +93,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
mut commands: Commands mut commands: Commands
| { | {
if trigger.event().button == PointerButton::Primary { if trigger.event().button == PointerButton::Primary {
commands.entity(trigger.target()).despawn(); commands.entity(trigger.target().unwrap()).despawn();
} }
}); });
} }

View File

@ -142,7 +142,7 @@ fn setup(mut commands: Commands) {
.observe( .observe(
|mut trigger: Trigger<Pointer<Click>>, |mut trigger: Trigger<Pointer<Click>>,
mut focus: ResMut<InputFocus>| { mut focus: ResMut<InputFocus>| {
focus.0 = Some(trigger.target()); focus.0 = Some(trigger.target().unwrap());
trigger.propagate(false); trigger.propagate(false);
}, },
); );

302
examples/ui/ui_transform.rs Normal file
View File

@ -0,0 +1,302 @@
//! An example demonstrating how to translate, rotate and scale UI elements.
use bevy::color::palettes::css::DARK_GRAY;
use bevy::color::palettes::css::RED;
use bevy::color::palettes::css::YELLOW;
use bevy::prelude::*;
use core::f32::consts::FRAC_PI_8;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, button_system)
.add_systems(Update, translation_system)
.run();
}
const NORMAL_BUTTON: Color = Color::WHITE;
const HOVERED_BUTTON: Color = Color::Srgba(YELLOW);
const PRESSED_BUTTON: Color = Color::Srgba(RED);
/// A button that rotates the target node
#[derive(Component)]
pub struct RotateButton(pub Rot2);
/// A button that scales the target node
#[derive(Component)]
pub struct ScaleButton(pub f32);
/// Marker component so the systems know which entities to translate, rotate and scale
#[derive(Component)]
pub struct TargetNode;
/// Handles button interactions
fn button_system(
mut interaction_query: Query<
(
&Interaction,
&mut BackgroundColor,
Option<&RotateButton>,
Option<&ScaleButton>,
),
(Changed<Interaction>, With<Button>),
>,
mut rotator_query: Query<&mut UiTransform, With<TargetNode>>,
) {
for (interaction, mut color, maybe_rotate, maybe_scale) in &mut interaction_query {
match *interaction {
Interaction::Pressed => {
*color = PRESSED_BUTTON.into();
if let Some(step) = maybe_rotate {
for mut transform in rotator_query.iter_mut() {
transform.rotation *= step.0;
}
}
if let Some(step) = maybe_scale {
for mut transform in rotator_query.iter_mut() {
transform.scale += step.0;
transform.scale =
transform.scale.clamp(Vec2::splat(0.25), Vec2::splat(3.0));
}
}
}
Interaction::Hovered => {
*color = HOVERED_BUTTON.into();
}
Interaction::None => {
*color = NORMAL_BUTTON.into();
}
}
}
}
// move the rotating panel when the arrow keys are pressed
fn translation_system(
time: Res<Time>,
input: Res<ButtonInput<KeyCode>>,
mut translation_query: Query<&mut UiTransform, With<TargetNode>>,
) {
let controls = [
(KeyCode::ArrowLeft, -Vec2::X),
(KeyCode::ArrowRight, Vec2::X),
(KeyCode::ArrowUp, -Vec2::Y),
(KeyCode::ArrowDown, Vec2::Y),
];
for &(key_code, direction) in &controls {
if input.pressed(key_code) {
for mut transform in translation_query.iter_mut() {
let d = direction * 50.0 * time.delta_secs();
let (Val::Px(x), Val::Px(y)) = (transform.translation.x, transform.translation.y)
else {
continue;
};
let x = (x + d.x).clamp(-150., 150.);
let y = (y + d.y).clamp(-150., 150.);
transform.translation = Val2::px(x, y);
}
}
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// UI camera
commands.spawn(Camera2d);
// Root node filling the whole screen
commands.spawn((
Node {
width: Val::Percent(100.),
height: Val::Percent(100.),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::BLACK),
children![(
Node {
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceEvenly,
column_gap: Val::Px(25.0),
row_gap: Val::Px(25.0),
..default()
},
BackgroundColor(Color::BLACK),
children![
(
Node {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
row_gap: Val::Px(10.0),
column_gap: Val::Px(10.0),
padding: UiRect::all(Val::Px(10.0)),
..default()
},
BackgroundColor(Color::BLACK),
GlobalZIndex(1),
children![
(
Button,
Node {
height: Val::Px(50.0),
width: Val::Px(50.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
RotateButton(Rot2::radians(-FRAC_PI_8)),
children![(Text::new("<--"), TextColor(Color::BLACK),)]
),
(
Button,
Node {
height: Val::Px(50.0),
width: Val::Px(50.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
ScaleButton(-0.25),
children![(Text::new("-"), TextColor(Color::BLACK),)]
),
]
),
// Target node with its own set of buttons
(
Node {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
width: Val::Px(300.0),
height: Val::Px(300.0),
..default()
},
BackgroundColor(DARK_GRAY.into()),
TargetNode,
children![
(
Button,
Node {
width: Val::Px(80.0),
height: Val::Px(80.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
children![(Text::new("Top"), TextColor(Color::BLACK))]
),
(
Node {
align_self: AlignSelf::Stretch,
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
..default()
},
children![
(
Button,
Node {
width: Val::Px(80.0),
height: Val::Px(80.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
UiTransform::from_rotation(Rot2::radians(
-std::f32::consts::FRAC_PI_2
)),
children![(Text::new("Left"), TextColor(Color::BLACK),)]
),
(
Node {
width: Val::Px(100.),
height: Val::Px(100.),
..Default::default()
},
ImageNode {
image: asset_server.load("branding/icon.png"),
image_mode: NodeImageMode::Stretch,
..default()
}
),
(
Button,
Node {
width: Val::Px(80.0),
height: Val::Px(80.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
UiTransform::from_rotation(Rot2::radians(
core::f32::consts::FRAC_PI_2
)),
BackgroundColor(Color::WHITE),
children![(Text::new("Right"), TextColor(Color::BLACK))]
),
]
),
(
Button,
Node {
width: Val::Px(80.0),
height: Val::Px(80.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
UiTransform::from_rotation(Rot2::radians(std::f32::consts::PI)),
children![(Text::new("Bottom"), TextColor(Color::BLACK),)]
),
]
),
// Right column of controls
(
Node {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
row_gap: Val::Px(10.0),
column_gap: Val::Px(10.0),
padding: UiRect::all(Val::Px(10.0)),
..default()
},
BackgroundColor(Color::BLACK),
GlobalZIndex(1),
children![
(
Button,
Node {
height: Val::Px(50.0),
width: Val::Px(50.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
RotateButton(Rot2::radians(FRAC_PI_8)),
children![(Text::new("-->"), TextColor(Color::BLACK),)]
),
(
Button,
Node {
height: Val::Px(50.0),
width: Val::Px(50.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(Color::WHITE),
ScaleButton(0.25),
children![(Text::new("+"), TextColor(Color::BLACK),)]
),
]
)
]
)],
));
}

View File

@ -91,7 +91,7 @@ fn test(
fn on_drag_viewport(drag: Trigger<Pointer<Drag>>, mut node_query: Query<&mut Node>) { fn on_drag_viewport(drag: Trigger<Pointer<Drag>>, mut node_query: Query<&mut Node>) {
if matches!(drag.button, PointerButton::Secondary) { if matches!(drag.button, PointerButton::Secondary) {
let mut node = node_query.get_mut(drag.target()).unwrap(); let mut node = node_query.get_mut(drag.target().unwrap()).unwrap();
if let (Val::Px(top), Val::Px(left)) = (node.top, node.left) { if let (Val::Px(top), Val::Px(left)) = (node.top, node.left) {
node.left = Val::Px(left + drag.delta.x); node.left = Val::Px(left + drag.delta.x);
@ -102,7 +102,7 @@ fn on_drag_viewport(drag: Trigger<Pointer<Drag>>, mut node_query: Query<&mut Nod
fn on_drag_cuboid(drag: Trigger<Pointer<Drag>>, mut transform_query: Query<&mut Transform>) { fn on_drag_cuboid(drag: Trigger<Pointer<Drag>>, mut transform_query: Query<&mut Transform>) {
if matches!(drag.button, PointerButton::Primary) { if matches!(drag.button, PointerButton::Primary) {
let mut transform = transform_query.get_mut(drag.target()).unwrap(); let mut transform = transform_query.get_mut(drag.target().unwrap()).unwrap();
transform.rotate_y(drag.delta.x * 0.02); transform.rotate_y(drag.delta.x * 0.02);
transform.rotate_x(drag.delta.y * 0.02); transform.rotate_x(drag.delta.y * 0.02);
} }

View File

@ -0,0 +1,12 @@
---
title: Observer Triggers
pull_requests: [19440]
---
Observers may be triggered on particular entities or globally.
Previously, a global trigger would claim to trigger on a particular `Entity`, `Entity::PLACEHOLDER`.
For correctness and transparency, triggers have been changed to `Option<Entity>`.
`Trigger::target` now returns `Option<Entity>` and `ObserverTrigger::target` is now of type `Option<Entity>`.
If you were checking for `Entity::PLACEHOLDER`, migrate to handling the `None` case.
If you were not checking for `Entity::PLACEHOLDER`, migrate to unwrapping, as `Entity::PLACEHOLDER` would have caused a panic before, at a later point.

View File

@ -0,0 +1,6 @@
---
title: RelativeCursorPosition is object-centered
pull_requests: [16615]
---
`RelativeCursorPosition`'s coordinates are now object-centered with (0,0) at the the center of the node and the corners at (±0.5, ±0.5). Its `normalized_visible_node_rect` field has been removed and replaced with a new `cursor_over: bool` field which is set to true when the cursor is hovering a visible section of the UI node.

View File

@ -0,0 +1,32 @@
---
title: Specialized UI transform
pull_requests: [16615]
---
Bevy UI now uses specialized 2D UI transform components `UiTransform` and `UiGlobalTransform` in place of `Transform` and `GlobalTransform`.
UiTransform is a 2d-only equivalent of Transform with a responsive translation in `Val`s. `UiGlobalTransform` newtypes `Affine2` and is updated in `ui_layout_system
`Node` now requires `UiTransform` instead of `Transform`. `UiTransform` requires `UiGlobalTransform`.
The `UiTransform` equivalent of the `Transform`:
```rust
Transform {
translation: Vec3 { x, y, z },
rotation:Quat::from_rotation_z(radians),
scale,
}
```
is
```rust
UiTransform {
translation: Val2::px(x, y),
rotation: Rot2::from_rotation(radians),
scale: scale.xy(),
}
```
In previous versions of Bevy `ui_layout_system` would overwrite UI node's `Transform::translation` each frame. `UiTransform`s aren't overwritten and there is no longer any need for systems that cache and rewrite the transform for translated UI elements.

View File

@ -0,0 +1,43 @@
---
title: Unified system state flag
pull_requests: [19506]
---
Now the system have a unified `SystemStateFlags` to represent its different states.
If your code previously looked like this:
```rust
impl System for MyCustomSystem {
// ...
fn is_send(&self) -> bool {
false
}
fn is_exclusive(&self) -> bool {
true
}
fn has_deferred(&self) -> bool {
false
}
// ....
}
```
You should migrate it to:
```rust
impl System for MyCustomSystem{
// ...
fn flags(&self) -> SystemStateFlags {
// non-send , exclusive , no deferred
SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE
}
// ...
}
```

View File

@ -0,0 +1,7 @@
---
title: Specialized UI Transform
authors: ["@Ickshonpe"]
pull_requests: [16615]
---
In Bevy UI `Transform` and `GlobalTransform` have been replaced by `UiTransform` and `UiGlobalTransform`. `UiTransform` is a specialized 2D UI transform which supports responsive translations.