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)"
wasm = true
[[example]]
name = "ui_transform"
path = "examples/ui/ui_transform.rs"
doc-scrape-examples = true
[package.metadata.example.ui_transform]
name = "UI Transform"
description = "An example demonstrating how to translate, rotate and scale UI elements."
category = "UI (User Interface)"
wasm = true
[[example]]
name = "viewport_debug"
path = "examples/ui/viewport_debug.rs"

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
use crate::{
bundle::Bundle,
entity::{hash_set::EntityHashSet, Entity},
prelude::Children,
relationship::{
Relationship, RelationshipHookMode, RelationshipSourceCollection, RelationshipTarget,
},
@ -302,6 +303,15 @@ impl<'w> EntityWorldMut<'w> {
self
}
/// Despawns the children of this entity.
/// This entity will not be despawned.
///
/// This is a specialization of [`despawn_related`](EntityWorldMut::despawn_related), a more general method for despawning via relationships.
pub fn despawn_children(&mut self) -> &mut Self {
self.despawn_related::<Children>();
self
}
/// Inserts a component or bundle of components into the entity and all related entities,
/// traversing the relationship tracked in `S` in a breadth-first manner.
///
@ -467,6 +477,14 @@ impl<'a> EntityCommands<'a> {
})
}
/// Despawns the children of this entity.
/// This entity will not be despawned.
///
/// This is a specialization of [`despawn_related`](EntityCommands::despawn_related), a more general method for despawning via relationships.
pub fn despawn_children(&mut self) -> &mut Self {
self.despawn_related::<Children>()
}
/// 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.
///

View File

@ -20,7 +20,7 @@ use crate::{
prelude::{IntoSystemSet, SystemSet},
query::{Access, FilteredAccessSet},
schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet},
system::{ScheduleSystem, System, SystemIn, SystemParamValidationError},
system::{ScheduleSystem, System, SystemIn, SystemParamValidationError, SystemStateFlags},
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World},
};
@ -171,26 +171,9 @@ impl System for ApplyDeferred {
const { &FilteredAccessSet::new() }
}
fn is_send(&self) -> bool {
// Although this system itself does nothing on its own, the system
// executor uses it to apply deferred commands. Commands must be allowed
// to access non-send resources, so this system must be non-send for
// scheduling purposes.
false
}
fn is_exclusive(&self) -> bool {
// This system is labeled exclusive because it is used by the system
// executor to find places where deferred commands should be applied,
// and commands can only be applied with exclusive access to the world.
true
}
fn has_deferred(&self) -> bool {
// This system itself doesn't have any commands to apply, but when it
// is pulled from the schedule to be ran, the executor will apply
// deferred commands from other systems.
false
fn flags(&self) -> SystemStateFlags {
// non-send , exclusive , no deferred
SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE
}
unsafe fn run_unsafe(

View File

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

View File

@ -1418,26 +1418,24 @@ impl ScheduleGraph {
if system_a.is_exclusive() || system_b.is_exclusive() {
conflicting_systems.push((a, b, Vec::new()));
} else {
let access_a = system_a.component_access();
let access_b = system_b.component_access();
if !access_a.is_compatible(access_b) {
match access_a.get_conflicts(access_b) {
AccessConflicts::Individual(conflicts) => {
let conflicts: Vec<_> = conflicts
.ones()
.map(ComponentId::get_sparse_set_index)
.filter(|id| !ignored_ambiguities.contains(id))
.collect();
if !conflicts.is_empty() {
conflicting_systems.push((a, b, conflicts));
}
}
AccessConflicts::All => {
// there is no specific component conflicting, but the systems are overall incompatible
// for example 2 systems with `Query<EntityMut>`
conflicting_systems.push((a, b, Vec::new()));
let access_a = system_a.component_access_set();
let access_b = system_b.component_access_set();
match access_a.get_conflicts(access_b) {
AccessConflicts::Individual(conflicts) => {
let conflicts: Vec<_> = conflicts
.ones()
.map(ComponentId::get_sparse_set_index)
.filter(|id| !ignored_ambiguities.contains(id))
.collect();
if !conflicts.is_empty() {
conflicting_systems.push((a, b, conflicts));
}
}
AccessConflicts::All => {
// there is no specific component conflicting, but the systems are overall incompatible
// for example 2 systems with `Query<EntityMut>`
conflicting_systems.push((a, b, Vec::new()));
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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()
}
/// Returns a [`QueryLens`] that can be used to get a query with a more general fetch.
/// Returns a [`QueryLens`] that can be used to construct a new [`Query`] giving more
/// restrictive access to the entities matched by the current query.
///
/// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`.
/// This can be useful for passing the query to another function. Note that since
/// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added),
/// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be
/// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`]
/// A transmute is valid only if `NewD` has a subset of the read, write, and required access
/// of the current query. A precise description of the access required by each parameter
/// type is given in the table below, but typical uses are to:
/// * Remove components, e.g. `Query<(&A, &B)>` to `Query<&A>`.
/// * Retrieve an existing component with reduced or equal access, e.g. `Query<&mut A>` to `Query<&A>`
/// or `Query<&T>` to `Query<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
///
/// This will panic if `NewD` is not a subset of the original fetch `D`
/// This will panic if the access required by `NewD` is not a subset of that required by
/// the original fetch `D`.
///
/// ## Example
///
@ -2065,30 +2115,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// # schedule.run(&mut world);
/// ```
///
/// ## Allowed Transmutes
///
/// Besides removing parameters from the query,
/// you can also make limited changes to the types of parameters.
/// The new query must have a subset of the *read*, *write*, and *required* access of the original query.
///
/// * `&mut T` and [`Mut<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
///
/// ```rust
@ -2165,28 +2191,21 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// // Nested inside of an `Or` filter, they have the same access as `Option<&T>`.
/// assert_valid_transmute_filtered::<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]
pub fn transmute_lens<NewD: QueryData>(&mut self) -> QueryLens<'_, 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.
///
/// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`.
/// This can be useful for passing the query to another function. Note that since
/// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added),
/// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be
/// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`]
/// See [`Self::transmute_lens`] for a description of allowed transmutes.
///
/// ## Panics
///
/// This will panic if `NewD` is not a subset of the original fetch `Q`
/// This will panic if `NewD` is not a subset of the original fetch `D`
///
/// ## Example
///
@ -2225,22 +2244,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// # schedule.run(&mut world);
/// ```
///
/// ## Allowed Transmutes
///
/// Besides removing parameters from the query, you can also
/// make limited changes to the types of parameters.
///
/// * Can always add/remove [`Entity`]
/// * Can always add/remove [`EntityLocation`]
/// * Can always add/remove [`&Archetype`]
/// * `Ref<T>` <-> `&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
///
/// - [`transmute_lens`](Self::transmute_lens) to convert to a lens using a mutable borrow of the [`Query`].
@ -2251,6 +2254,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// Equivalent to [`Self::transmute_lens`] but also includes a [`QueryFilter`] type.
///
/// See [`Self::transmute_lens`] for a description of allowed transmutes.
///
/// Note that the lens will iterate the same tables and archetypes as the original query. This means that
/// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without)
/// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added),
@ -2266,10 +2271,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// Equivalent to [`Self::transmute_lens_inner`] but also includes a [`QueryFilter`] type.
/// This consumes the [`Query`] to return results with the actual "inner" world lifetime.
///
/// See [`Self::transmute_lens`] for a description of allowed transmutes.
///
/// Note that the lens will iterate the same tables and archetypes as the original query. This means that
/// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without)
/// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added),
/// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will only be respected if they
/// are in the type signature.
///
/// # See also
///

View File

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

View File

@ -2,6 +2,7 @@
clippy::module_inception,
reason = "This instance of module inception is being discussed; see #17353."
)]
use bitflags::bitflags;
use core::fmt::Debug;
use log::warn;
use thiserror::Error;
@ -19,6 +20,18 @@ use core::any::TypeId;
use super::{IntoSystem, SystemParamValidationError};
bitflags! {
/// Bitflags representing system states and requirements.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct SystemStateFlags: u8 {
/// Set if system cannot be sent across threads
const NON_SEND = 1 << 0;
/// Set if system requires exclusive World access
const EXCLUSIVE = 1 << 1;
/// Set if system has deferred buffers.
const DEFERRED = 1 << 2;
}
}
/// An ECS system that can be added to a [`Schedule`](crate::schedule::Schedule)
///
/// Systems are functions with all arguments implementing
@ -50,14 +63,26 @@ pub trait System: Send + Sync + 'static {
/// Returns the system's component [`FilteredAccessSet`].
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId>;
/// Returns the [`SystemStateFlags`] of the system.
fn flags(&self) -> SystemStateFlags;
/// Returns true if the system is [`Send`].
fn is_send(&self) -> bool;
#[inline]
fn is_send(&self) -> bool {
!self.flags().intersects(SystemStateFlags::NON_SEND)
}
/// Returns true if the system must be run exclusively.
fn is_exclusive(&self) -> bool;
#[inline]
fn is_exclusive(&self) -> bool {
self.flags().intersects(SystemStateFlags::EXCLUSIVE)
}
/// Returns true if system has deferred buffers.
fn has_deferred(&self) -> bool;
#[inline]
fn has_deferred(&self) -> bool {
self.flags().intersects(SystemStateFlags::DEFERRED)
}
/// Runs the system with the given input in the world. Unlike [`System::run`], this function
/// can be called in parallel with other systems and may break Rust's aliasing rules

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

View File

@ -2371,7 +2371,7 @@ impl<'w> EntityWorldMut<'w> {
if archetype.has_despawn_observer() {
deferred_world.trigger_observers(
ON_DESPAWN,
self.entity,
Some(self.entity),
archetype.components(),
caller,
);
@ -2385,7 +2385,7 @@ impl<'w> EntityWorldMut<'w> {
if archetype.has_replace_observer() {
deferred_world.trigger_observers(
ON_REPLACE,
self.entity,
Some(self.entity),
archetype.components(),
caller,
);
@ -2400,7 +2400,7 @@ impl<'w> EntityWorldMut<'w> {
if archetype.has_remove_observer() {
deferred_world.trigger_observers(
ON_REMOVE,
self.entity,
Some(self.entity),
archetype.components(),
caller,
);
@ -5749,7 +5749,9 @@ mod tests {
let entity = world
.spawn_empty()
.observe(|trigger: Trigger<TestEvent>, mut commands: Commands| {
commands.entity(trigger.target()).insert(TestComponent(0));
commands
.entity(trigger.target().unwrap())
.insert(TestComponent(0));
})
.id();
@ -5769,7 +5771,7 @@ mod tests {
let mut world = World::new();
world.add_observer(
|trigger: Trigger<OnAdd, TestComponent>, mut commands: Commands| {
commands.entity(trigger.target()).despawn();
commands.entity(trigger.target().unwrap()).despawn();
},
);
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)]
pub enum ImageFormatSetting {
/// Determine the image format from its file extension.
///
/// This is the default.
#[default]
FromExtension,
/// Declare the image format explicitly.
Format(ImageFormat),
/// Guess the image format by looking for magic bytes at the
/// beginning of its data.
Guess,
}
/// Settings for loading an [`Image`] using an [`ImageLoader`].
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ImageLoaderSettings {
/// How to determine the image's format.
pub format: ImageFormatSetting,
/// Specifies whether image data is linear
/// or in sRGB space when this is not determined by
/// the image format.
pub is_srgb: bool,
/// [`ImageSampler`] to use when rendering - this does
/// not affect the loading of the image data.
pub sampler: ImageSampler,
/// Where the asset will be used - see the docs on
/// [`RenderAssetUsages`] for details.
pub asset_usage: RenderAssetUsages,
}
@ -108,11 +124,14 @@ impl Default for ImageLoaderSettings {
}
}
/// An error when loading an image using [`ImageLoader`].
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum ImageLoaderError {
#[error("Could load shader: {0}")]
/// An error occurred while trying to load the image bytes.
#[error("Failed to load image bytes: {0}")]
Io(#[from] std::io::Error),
/// An error occurred while trying to decode the image bytes.
#[error("Could not load texture file: {0}")]
FileTexture(#[from] FileTextureError),
}
@ -170,7 +189,7 @@ impl AssetLoader for ImageLoader {
/// An error that occurs when loading a texture from a file.
#[derive(Error, Debug)]
#[error("Error reading image file {path}: {error}, this is an error in `bevy_render`.")]
#[error("Error reading image file {path}: {error}.")]
pub struct FileTextureError {
error: TextureError,
path: String,

View File

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

View File

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

View File

@ -208,18 +208,6 @@ pub fn update_interactions(
mut pointers: Query<(&PointerId, &PointerPress, &mut PointerInteraction)>,
mut interact: Query<&mut PickingInteraction>,
) {
// Clear all previous hover data from pointers and entities
for (pointer, _, mut pointer_interaction) in &mut pointers {
pointer_interaction.sorted_entities.clear();
if let Some(previously_hovered_entities) = previous_hover_map.get(pointer) {
for entity in previously_hovered_entities.keys() {
if let Ok(mut interaction) = interact.get_mut(*entity) {
*interaction = PickingInteraction::None;
}
}
}
}
// Create a map to hold the aggregated interaction for each entity. This is needed because we
// need to be able to insert the interaction component on entities if they do not exist. To do
// so we need to know the final aggregated interaction state to avoid the scenario where we set
@ -239,13 +227,29 @@ pub fn update_interactions(
}
// Take the aggregated entity states and update or insert the component if missing.
for (hovered_entity, new_interaction) in new_interaction_state.drain() {
for (&hovered_entity, &new_interaction) in new_interaction_state.iter() {
if let Ok(mut interaction) = interact.get_mut(hovered_entity) {
*interaction = new_interaction;
interaction.set_if_neq(new_interaction);
} else if let Ok(mut entity_commands) = commands.get_entity(hovered_entity) {
entity_commands.try_insert(new_interaction);
}
}
// Clear all previous hover data from pointers that are no longer hovering any entities.
// We do this last to preserve change detection for picking interactions.
for (pointer, _, _) in &mut pointers {
let Some(previously_hovered_entities) = previous_hover_map.get(pointer) else {
continue;
};
for entity in previously_hovered_entities.keys() {
if !new_interaction_state.contains_key(entity) {
if let Ok(mut interaction) = interact.get_mut(*entity) {
interaction.set_if_neq(PickingInteraction::None);
}
}
}
}
}
/// Merge the interaction state of this entity into the aggregated map.

View File

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

View File

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

View File

@ -1060,6 +1060,7 @@ pub fn camera_system(
#[reflect(opaque)]
#[reflect(Component, Default, Clone)]
pub struct CameraMainTextureUsages(pub TextureUsages);
impl Default for CameraMainTextureUsages {
fn default() -> Self {
Self(
@ -1070,6 +1071,13 @@ impl Default for CameraMainTextureUsages {
}
}
impl CameraMainTextureUsages {
pub fn with(mut self, usages: TextureUsages) -> Self {
self.0 |= usages;
self
}
}
#[derive(Component, Debug)]
pub struct ExtractedCamera {
pub target: Option<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] {
#[inline]
fn into_binding(self) -> BindingResource<'a> {
@ -161,6 +168,13 @@ impl<'a> IntoBinding<'a> for &'a Sampler {
}
}
impl<'a> IntoBinding<'a> for &'a [&'a wgpu::Sampler] {
#[inline]
fn into_binding(self) -> BindingResource<'a> {
BindingResource::SamplerArray(self)
}
}
impl<'a> IntoBinding<'a> for BindingResource<'a> {
#[inline]
fn into_binding(self) -> BindingResource<'a> {
@ -175,6 +189,13 @@ impl<'a> IntoBinding<'a> for wgpu::BufferBinding<'a> {
}
}
impl<'a> IntoBinding<'a> for &'a [wgpu::BufferBinding<'a>] {
#[inline]
fn into_binding(self) -> BindingResource<'a> {
BindingResource::BufferArray(self)
}
}
pub trait IntoBindingArray<'b, const N: usize> {
fn into_array(self) -> [BindingResource<'b>; N];
}

View File

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

View File

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

View File

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

View File

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

View File

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

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::{
change_detection::DetectChangesMut,
entity::{ContainsEntity, Entity},
hierarchy::ChildOf,
prelude::{Component, With},
query::QueryData,
reflect::ReflectComponent,
system::{Local, Query, Res},
};
use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput};
use bevy_math::{Rect, Vec2};
use bevy_math::Vec2;
use bevy_platform::collections::HashMap;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::InheritedVisibility};
use bevy_transform::components::GlobalTransform;
use bevy_window::{PrimaryWindow, Window};
use smallvec::SmallVec;
@ -67,12 +70,12 @@ impl Default for Interaction {
}
}
/// A component storing the position of the mouse relative to the node, (0., 0.) being the top-left corner and (1., 1.) being the bottom-right
/// If the mouse is not over the node, the value will go beyond the range of (0., 0.) to (1., 1.)
/// A component storing the position of the mouse relative to the node, (0., 0.) being the center and (0.5, 0.5) being the bottom-right
/// If the mouse is not over the node, the value will go beyond the range of (-0.5, -0.5) to (0.5, 0.5)
///
/// It can be used alongside [`Interaction`] to get the position of the press.
///
/// The component is updated when it is in the same entity with [`Node`](crate::Node).
/// The component is updated when it is in the same entity with [`Node`].
#[derive(Component, Copy, Clone, Default, PartialEq, Debug, Reflect)]
#[reflect(Component, Default, PartialEq, Debug, Clone)]
#[cfg_attr(
@ -81,8 +84,8 @@ impl Default for Interaction {
reflect(Serialize, Deserialize)
)]
pub struct RelativeCursorPosition {
/// Visible area of the Node relative to the size of the entire Node.
pub normalized_visible_node_rect: Rect,
/// True if the cursor position is over an unclipped area of the Node.
pub cursor_over: bool,
/// Cursor position relative to the size and position of the Node.
/// A None value indicates that the cursor position is unknown.
pub normalized: Option<Vec2>,
@ -90,9 +93,8 @@ pub struct RelativeCursorPosition {
impl RelativeCursorPosition {
/// A helper function to check if the mouse is over the node
pub fn mouse_over(&self) -> bool {
self.normalized
.is_some_and(|position| self.normalized_visible_node_rect.contains(position))
pub fn cursor_over(&self) -> bool {
self.cursor_over
}
}
@ -133,11 +135,10 @@ pub struct State {
pub struct NodeQuery {
entity: Entity,
node: &'static ComputedNode,
global_transform: &'static GlobalTransform,
transform: &'static UiGlobalTransform,
interaction: Option<&'static mut Interaction>,
relative_cursor_position: Option<&'static mut RelativeCursorPosition>,
focus_policy: Option<&'static FocusPolicy>,
calculated_clip: Option<&'static CalculatedClip>,
inherited_visibility: Option<&'static InheritedVisibility>,
target_camera: &'static ComputedNodeTarget,
}
@ -154,6 +155,8 @@ pub fn ui_focus_system(
touches_input: Res<Touches>,
ui_stack: Res<UiStack>,
mut node_query: Query<NodeQuery>,
clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>,
child_of_query: Query<&ChildOf>,
) {
let primary_window = primary_window.iter().next();
@ -234,46 +237,30 @@ pub fn ui_focus_system(
}
let camera_entity = node.target_camera.camera()?;
let node_rect = Rect::from_center_size(
node.global_transform.translation().truncate(),
node.node.size(),
);
// Intersect with the calculated clip rect to find the bounds of the visible region of the node
let visible_rect = node
.calculated_clip
.map(|clip| node_rect.intersect(clip.clip))
.unwrap_or(node_rect);
let cursor_position = camera_cursor_positions.get(&camera_entity);
let contains_cursor = cursor_position.is_some_and(|point| {
node.node.contains_point(*node.transform, *point)
&& clip_check_recursive(*point, *entity, &clipping_query, &child_of_query)
});
// The mouse position relative to the node
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
// (-0.5, -0.5) is the top-left corner, (0.5, 0.5) is the bottom-right corner
// Coordinates are relative to the entire node, not just the visible region.
let relative_cursor_position = cursor_position.and_then(|cursor_position| {
let normalized_cursor_position = cursor_position.and_then(|cursor_position| {
// ensure node size is non-zero in all dimensions, otherwise relative position will be
// +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to
// false positives for mouse_over (#12395)
(node_rect.size().cmpgt(Vec2::ZERO).all())
.then_some((*cursor_position - node_rect.min) / node_rect.size())
node.node.normalize_point(*node.transform, *cursor_position)
});
// If the current cursor position is within the bounds of the node's visible area, consider it for
// clicking
let relative_cursor_position_component = RelativeCursorPosition {
normalized_visible_node_rect: visible_rect.normalize(node_rect),
normalized: relative_cursor_position,
cursor_over: contains_cursor,
normalized: normalized_cursor_position,
};
let contains_cursor = relative_cursor_position_component.mouse_over()
&& cursor_position.is_some_and(|point| {
pick_rounded_rect(
*point - node_rect.center(),
node_rect.size(),
node.node.border_radius,
)
});
// Save the relative cursor position to the correct component
if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position
{
@ -284,7 +271,8 @@ pub fn ui_focus_system(
Some(*entity)
} else {
if let Some(mut interaction) = node.interaction {
if *interaction == Interaction::Hovered || (relative_cursor_position.is_none())
if *interaction == Interaction::Hovered
|| (normalized_cursor_position.is_none())
{
interaction.set_if_neq(Interaction::None);
}
@ -334,26 +322,3 @@ pub fn ui_focus_system(
}
}
}
// Returns true if `point` (relative to the rectangle's center) is within the bounds of a rounded rectangle with
// the given size and border radius.
//
// Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles.
pub(crate) fn pick_rounded_rect(
point: Vec2,
size: Vec2,
border_radius: ResolvedBorderRadius,
) -> bool {
let [top, bottom] = if point.x < 0. {
[border_radius.top_left, border_radius.bottom_left]
} else {
[border_radius.top_right, border_radius.bottom_right]
};
let r = if point.y < 0. { top } else { bottom };
let corner_to_point = point.abs() - 0.5 * size;
let q = corner_to_point + r;
let l = q.max(Vec2::ZERO).length();
let m = q.max_element().min(0.);
l + m - r < 0.
}

View File

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

View File

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

View File

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

View File

@ -24,14 +24,13 @@
#![deny(missing_docs)]
use crate::{focus::pick_rounded_rect, prelude::*, UiStack};
use crate::{prelude::*, ui_transform::UiGlobalTransform, UiStack};
use bevy_app::prelude::*;
use bevy_ecs::{prelude::*, query::QueryData};
use bevy_math::{Rect, Vec2};
use bevy_math::Vec2;
use bevy_platform::collections::HashMap;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::prelude::*;
use bevy_transform::prelude::*;
use bevy_window::PrimaryWindow;
use bevy_picking::backend::prelude::*;
@ -91,9 +90,8 @@ impl Plugin for UiPickingPlugin {
pub struct NodeQuery {
entity: Entity,
node: &'static ComputedNode,
global_transform: &'static GlobalTransform,
transform: &'static UiGlobalTransform,
pickable: Option<&'static Pickable>,
calculated_clip: Option<&'static CalculatedClip>,
inherited_visibility: Option<&'static InheritedVisibility>,
target_camera: &'static ComputedNodeTarget,
}
@ -110,6 +108,8 @@ pub fn ui_picking(
ui_stack: Res<UiStack>,
node_query: Query<NodeQuery>,
mut output: EventWriter<PointerHits>,
clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>,
child_of_query: Query<&ChildOf>,
) {
// For each camera, the pointer and its position
let mut pointer_pos_by_camera = HashMap::<Entity, HashMap<PointerId, Vec2>>::default();
@ -181,43 +181,33 @@ pub fn ui_picking(
continue;
};
let node_rect = Rect::from_center_size(
node.global_transform.translation().truncate(),
node.node.size(),
);
// Nodes with Display::None have a (0., 0.) logical rect and can be ignored
if node_rect.size() == Vec2::ZERO {
if node.node.size() == Vec2::ZERO {
continue;
}
// Intersect with the calculated clip rect to find the bounds of the visible region of the node
let visible_rect = node
.calculated_clip
.map(|clip| node_rect.intersect(clip.clip))
.unwrap_or(node_rect);
let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity);
// The mouse position relative to the node
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
// Find the normalized cursor position relative to the node.
// (±0., 0.) is the center with the corners at points (±0.5, ±0.5).
// Coordinates are relative to the entire node, not just the visible region.
for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) {
let relative_cursor_position = (*cursor_position - node_rect.min) / node_rect.size();
if visible_rect
.normalize(node_rect)
.contains(relative_cursor_position)
&& pick_rounded_rect(
*cursor_position - node_rect.center(),
node_rect.size(),
node.node.border_radius,
if node.node.contains_point(*node.transform, *cursor_position)
&& clip_check_recursive(
*cursor_position,
*node_entity,
&clipping_query,
&child_of_query,
)
{
hit_nodes
.entry((camera_entity, *pointer_id))
.or_default()
.push((*node_entity, relative_cursor_position));
.push((
*node_entity,
node.transform.inverse().transform_point2(*cursor_position)
/ node.node.size(),
));
}
}
}
@ -262,3 +252,27 @@ pub fn ui_picking(
output.write(PointerHits::new(*pointer, picks, order));
}
}
/// Walk up the tree child-to-parent checking that `point` is not clipped by any ancestor node.
pub fn clip_check_recursive(
point: Vec2,
entity: Entity,
clipping_query: &Query<'_, '_, (&ComputedNode, &UiGlobalTransform, &Node)>,
child_of_query: &Query<&ChildOf>,
) -> bool {
if let Ok(child_of) = child_of_query.get(entity) {
let parent = child_of.0;
if let Ok((computed_node, transform, node)) = clipping_query.get(parent) {
if !computed_node
.resolve_clip_rect(node.overflow, node.overflow_clip_margin)
.contains(transform.inverse().transform_point2(point))
{
// The point is clipped and should be ignored by picking
return false;
}
}
return clip_check_recursive(point, parent, clipping_query, child_of_query);
}
// Reached root, point unclipped by all ancestors
true
}

View File

@ -2,6 +2,7 @@
use core::{hash::Hash, ops::Range};
use crate::prelude::UiGlobalTransform;
use crate::{
BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystems,
ResolvedBorderRadius, TransparentUi, Val,
@ -18,7 +19,7 @@ use bevy_ecs::{
},
};
use bevy_image::BevyDefault as _;
use bevy_math::{vec2, FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles};
use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2};
use bevy_render::sync_world::MainEntity;
use bevy_render::RenderApp;
use bevy_render::{
@ -29,7 +30,6 @@ use bevy_render::{
view::*,
Extract, ExtractSchedule, Render, RenderSystems,
};
use bevy_transform::prelude::GlobalTransform;
use bytemuck::{Pod, Zeroable};
use super::{stack_z_offsets, UiCameraMap, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS};
@ -211,7 +211,7 @@ impl SpecializedRenderPipeline for BoxShadowPipeline {
/// Description of a shadow to be sorted and queued for rendering
pub struct ExtractedBoxShadow {
pub stack_index: u32,
pub transform: Mat4,
pub transform: Affine2,
pub bounds: Vec2,
pub clip: Option<Rect>,
pub extracted_camera_entity: Entity,
@ -236,7 +236,7 @@ pub fn extract_shadows(
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
&InheritedVisibility,
&BoxShadow,
Option<&CalculatedClip>,
@ -302,7 +302,7 @@ pub fn extract_shadows(
extracted_box_shadows.box_shadows.push(ExtractedBoxShadow {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.compute_matrix() * Mat4::from_translation(offset.extend(0.)),
transform: Affine2::from(transform) * Affine2::from_translation(offset),
color: drop_shadow.color.into(),
bounds: shadow_size + 6. * blur_radius,
clip: clip.map(|clip| clip.clip),
@ -405,11 +405,15 @@ pub fn prepare_shadows(
.get(item.index)
.filter(|n| item.entity() == n.render_entity)
{
let rect_size = box_shadow.bounds.extend(1.0);
let rect_size = box_shadow.bounds;
// Specify the corners of the node
let positions = QUAD_VERTEX_POSITIONS
.map(|pos| (box_shadow.transform * (pos * rect_size).extend(1.)).xyz());
let positions = QUAD_VERTEX_POSITIONS.map(|pos| {
box_shadow
.transform
.transform_point2(pos * rect_size)
.extend(0.)
});
// Calculate the effect of clipping
// Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)
@ -443,7 +447,7 @@ pub fn prepare_shadows(
positions[3] + positions_diff[3].extend(0.),
];
let transformed_rect_size = box_shadow.transform.transform_vector3(rect_size);
let transformed_rect_size = box_shadow.transform.transform_vector2(rect_size);
// Don't try to cull nodes that have a rotation
// In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π
@ -492,7 +496,7 @@ pub fn prepare_shadows(
size: box_shadow.size.into(),
radius,
blur: box_shadow.blur_radius,
bounds: rect_size.xy().into(),
bounds: rect_size.into(),
});
}

View File

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

View File

@ -17,8 +17,9 @@ use bevy_ecs::{
use bevy_image::prelude::*;
use bevy_math::{
ops::{cos, sin},
FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles,
FloatOrd, Rect, Vec2,
};
use bevy_math::{Affine2, Vec2Swizzles};
use bevy_render::sync_world::MainEntity;
use bevy_render::{
render_phase::*,
@ -29,7 +30,6 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderSystems,
};
use bevy_sprite::BorderRect;
use bevy_transform::prelude::GlobalTransform;
use bytemuck::{Pod, Zeroable};
use super::shader_flags::BORDER_ALL;
@ -238,7 +238,7 @@ pub enum ResolvedGradient {
pub struct ExtractedGradient {
pub stack_index: u32,
pub transform: Mat4,
pub transform: Affine2,
pub rect: Rect,
pub clip: Option<Rect>,
pub extracted_camera_entity: Entity,
@ -354,7 +354,7 @@ pub fn extract_gradients(
Entity,
&ComputedNode,
&ComputedNodeTarget,
&GlobalTransform,
&UiGlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
AnyOf<(&BackgroundGradient, &BorderGradient)>,
@ -414,7 +414,7 @@ pub fn extract_gradients(
border_radius: uinode.border_radius,
border: uinode.border,
node_type,
transform: transform.compute_matrix(),
transform: transform.into(),
},
main_entity: entity.into(),
render_entity: commands.spawn(TemporaryRenderEntity).id(),
@ -439,7 +439,7 @@ pub fn extract_gradients(
extracted_gradients.items.push(ExtractedGradient {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.compute_matrix(),
transform: transform.into(),
stops_range: range_start..extracted_color_stops.0.len(),
rect: Rect {
min: Vec2::ZERO,
@ -487,7 +487,7 @@ pub fn extract_gradients(
extracted_gradients.items.push(ExtractedGradient {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.compute_matrix(),
transform: transform.into(),
stops_range: range_start..extracted_color_stops.0.len(),
rect: Rect {
min: Vec2::ZERO,
@ -541,7 +541,7 @@ pub fn extract_gradients(
extracted_gradients.items.push(ExtractedGradient {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.compute_matrix(),
transform: transform.into(),
stops_range: range_start..extracted_color_stops.0.len(),
rect: Rect {
min: Vec2::ZERO,
@ -675,12 +675,16 @@ pub fn prepare_gradient(
*item.batch_range_mut() = item_index as u32..item_index as u32 + 1;
let uinode_rect = gradient.rect;
let rect_size = uinode_rect.size().extend(1.0);
let rect_size = uinode_rect.size();
// Specify the corners of the node
let positions = QUAD_VERTEX_POSITIONS
.map(|pos| (gradient.transform * (pos * rect_size).extend(1.)).xyz());
let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy());
let positions = QUAD_VERTEX_POSITIONS.map(|pos| {
gradient
.transform
.transform_point2(pos * rect_size)
.extend(0.)
});
let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size);
// Calculate the effect of clipping
// Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)
@ -721,7 +725,7 @@ pub fn prepare_gradient(
corner_points[3] + positions_diff[3],
];
let transformed_rect_size = gradient.transform.transform_vector3(rect_size);
let transformed_rect_size = gradient.transform.transform_vector2(rect_size);
// Don't try to cull nodes that have a rotation
// In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π

View File

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

View File

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

View File

@ -1,5 +1,6 @@
use core::{hash::Hash, ops::Range};
use crate::prelude::UiGlobalTransform;
use crate::*;
use bevy_asset::*;
use bevy_color::{Alpha, ColorToComponents, LinearRgba};
@ -11,7 +12,7 @@ use bevy_ecs::{
},
};
use bevy_image::prelude::*;
use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles};
use bevy_math::{Affine2, FloatOrd, Rect, Vec2};
use bevy_platform::collections::HashMap;
use bevy_render::sync_world::MainEntity;
use bevy_render::{
@ -25,7 +26,6 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderSystems,
};
use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer};
use bevy_transform::prelude::GlobalTransform;
use binding_types::{sampler, texture_2d};
use bytemuck::{Pod, Zeroable};
use widget::ImageNode;
@ -218,7 +218,7 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline {
pub struct ExtractedUiTextureSlice {
pub stack_index: u32,
pub transform: Mat4,
pub transform: Affine2,
pub rect: Rect,
pub atlas_rect: Option<Rect>,
pub image: AssetId<Image>,
@ -246,7 +246,7 @@ pub fn extract_ui_texture_slices(
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedNodeTarget,
@ -306,7 +306,7 @@ pub fn extract_ui_texture_slices(
extracted_ui_slicers.slices.push(ExtractedUiTextureSlice {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.compute_matrix(),
transform: transform.into(),
color: image.color.into(),
rect: Rect {
min: Vec2::ZERO,
@ -497,11 +497,12 @@ pub fn prepare_ui_slices(
let uinode_rect = texture_slices.rect;
let rect_size = uinode_rect.size().extend(1.0);
let rect_size = uinode_rect.size();
// Specify the corners of the node
let positions = QUAD_VERTEX_POSITIONS
.map(|pos| (texture_slices.transform * (pos * rect_size).extend(1.)).xyz());
let positions = QUAD_VERTEX_POSITIONS.map(|pos| {
(texture_slices.transform.transform_point2(pos * rect_size)).extend(0.)
});
// Calculate the effect of clipping
// Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)
@ -536,7 +537,7 @@ pub fn prepare_ui_slices(
];
let transformed_rect_size =
texture_slices.transform.transform_vector3(rect_size);
texture_slices.transform.transform_vector2(rect_size);
// Don't try to cull nodes that have a rotation
// In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π

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_derive::{Deref, DerefMut};
use bevy_ecs::{prelude::*, system::SystemParam};
@ -9,7 +12,6 @@ use bevy_render::{
view::Visibility,
};
use bevy_sprite::BorderRect;
use bevy_transform::components::Transform;
use bevy_utils::once;
use bevy_window::{PrimaryWindow, WindowRef};
use core::{f32, num::NonZero};
@ -229,6 +231,73 @@ impl ComputedNode {
pub const fn inverse_scale_factor(&self) -> f32 {
self.inverse_scale_factor
}
// Returns true if `point` within the node.
//
// Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles.
pub fn contains_point(&self, transform: UiGlobalTransform, point: Vec2) -> bool {
let Some(local_point) = transform
.try_inverse()
.map(|transform| transform.transform_point2(point))
else {
return false;
};
let [top, bottom] = if local_point.x < 0. {
[self.border_radius.top_left, self.border_radius.bottom_left]
} else {
[
self.border_radius.top_right,
self.border_radius.bottom_right,
]
};
let r = if local_point.y < 0. { top } else { bottom };
let corner_to_point = local_point.abs() - 0.5 * self.size;
let q = corner_to_point + r;
let l = q.max(Vec2::ZERO).length();
let m = q.max_element().min(0.);
l + m - r < 0.
}
/// Transform a point to normalized node space with the center of the node at the origin and the corners at [+/-0.5, +/-0.5]
pub fn normalize_point(&self, transform: UiGlobalTransform, point: Vec2) -> Option<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 {
@ -323,12 +392,12 @@ impl From<Vec2> for ScrollPosition {
#[require(
ComputedNode,
ComputedNodeTarget,
UiTransform,
BackgroundColor,
BorderColor,
BorderRadius,
FocusPolicy,
ScrollPosition,
Transform,
Visibility,
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::{
experimental::{UiChildren, UiRootNodes},
ui_transform::UiGlobalTransform,
CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale,
UiTargetCamera,
};
@ -17,7 +18,6 @@ use bevy_ecs::{
use bevy_math::{Rect, UVec2};
use bevy_render::camera::Camera;
use bevy_sprite::BorderRect;
use bevy_transform::components::GlobalTransform;
/// Updates clipping for all nodes
pub fn update_clipping_system(
@ -26,7 +26,7 @@ pub fn update_clipping_system(
mut node_query: Query<(
&Node,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
Option<&mut CalculatedClip>,
)>,
ui_children: UiChildren,
@ -48,14 +48,13 @@ fn update_clipping(
node_query: &mut Query<(
&Node,
&ComputedNode,
&GlobalTransform,
&UiGlobalTransform,
Option<&mut CalculatedClip>,
)>,
entity: Entity,
mut maybe_inherited_clip: Option<Rect>,
) {
let Ok((node, computed_node, global_transform, maybe_calculated_clip)) =
node_query.get_mut(entity)
let Ok((node, computed_node, transform, maybe_calculated_clip)) = node_query.get_mut(entity)
else {
return;
};
@ -91,10 +90,7 @@ fn update_clipping(
maybe_inherited_clip
} else {
// Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists
let mut clip_rect = Rect::from_center_size(
global_transform.translation().truncate(),
computed_node.size(),
);
let mut clip_rect = Rect::from_center_size(transform.translation, computed_node.size());
// Content isn't clipped at the edges of the node but at the edges of the region specified by [`Node::overflow_clip_margin`].
//

View File

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

View File

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

View File

@ -65,12 +65,12 @@ fn change_material(
mut asset_materials: ResMut<Assets<StandardMaterial>>,
) {
// Get the `ColorOverride` of the entity, if it does not have a color override, skip
let Ok(color_override) = color_override.get(trigger.target()) else {
let Ok(color_override) = color_override.get(trigger.target().unwrap()) else {
return;
};
// Iterate over all children recursively
for descendants in children.iter_descendants(trigger.target()) {
for descendants in children.iter_descendants(trigger.target().unwrap()) {
// Get the material of the descendant
if let Some(material) = mesh_materials
.get(descendants)

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 Slice](../examples/ui/ui_texture_slice.rs) | Illustrates how to use 9 Slicing in UI
[UI Texture Slice Flipping and Tiling](../examples/ui/ui_texture_slice_flip_and_tile.rs) | Illustrates how to flip and tile images with 9 Slicing in UI
[UI Transform](../examples/ui/ui_transform.rs) | An example demonstrating how to translate, rotate and scale UI elements.
[UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements
[Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates
[Viewport Node](../examples/ui/viewport_node.rs) | Demonstrates how to create a viewport node with picking support

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.
// Start by finding the AnimationToPlay component we added to that entity.
if let Ok(animation_to_play) = animations_to_play.get(trigger.target()) {
if let Ok(animation_to_play) = animations_to_play.get(trigger.target().unwrap()) {
// The SceneRoot component will have spawned the scene as a hierarchy
// of entities parented to our entity. Since the asset contained a skinned
// mesh and animations, it will also have spawned an animation player
// component. Search our entity's descendants to find the animation player.
for child in children.iter_descendants(trigger.target()) {
for child in children.iter_descendants(trigger.target().unwrap()) {
if let Ok(mut player) = players.get_mut(child) {
// Tell the animation player to start the animation and keep
// repeating it.

View File

@ -47,7 +47,10 @@ fn observe_on_step(
transforms: Query<&GlobalTransform>,
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.
for _ in 0..14 {
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>,
mut commands: Commands,
) {
let clicked_entity = trigger.target();
let clicked_entity = trigger.target().unwrap();
// Windows and text are entities and can be clicked!
// We definitely don't want to disable the window itself,
// because that would cause the app to close!

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

View File

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

View File

@ -50,7 +50,7 @@ fn remove_component(
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.
let entity = trigger.target();
let entity = trigger.target().unwrap();
if let Ok(mut sprite) = query.get_mut(entity) {
sprite.color = Color::srgb(0.5, 1., 1.);
}

View File

@ -127,7 +127,10 @@ fn tick_timers(
}
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>>() {
target.insert(bundle);
}

View File

@ -48,13 +48,13 @@ fn setup_scene(
.observe(on_click_spawn_cube)
.observe(
|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;
},
)
.observe(
|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();
},
);
@ -102,7 +102,7 @@ fn on_click_spawn_cube(
}
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_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
// material. Instead, the event type is a generic, and the material is passed in.
move |trigger, mut query| {
if let Ok(mut material) = query.get_mut(trigger.target()) {
if let Ok(mut material) = query.get_mut(trigger.target().unwrap()) {
material.0 = new_material.clone();
}
}
@ -191,7 +191,7 @@ fn rotate(mut query: Query<&mut Transform, With<Shape>>, time: Res<Time>) {
/// An observer to rotate an entity when it is dragged
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_x(drag.delta.y * 0.02);
}

View File

@ -27,13 +27,13 @@ fn setup_scene(
.observe(on_click_spawn_cube)
.observe(
|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;
},
)
.observe(
|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();
},
);
@ -80,7 +80,7 @@ fn on_click_spawn_cube(
}
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_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.
fn recolor_on<E: Debug + Clone + Reflect>(color: Color) -> impl Fn(Trigger<E>, Query<&mut Sprite>) {
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;
};
sprite.color = color;

View File

@ -281,7 +281,7 @@ mod animation {
animation: Res<Animation>,
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) {
let mut transitions = AnimationTransitions::new();
transitions

View File

@ -228,7 +228,13 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
parent.spawn((
ImageNode::new(asset_server.load("branding/bevy_logo_light.png")),
// 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.)),
Outline {
width: Val::Px(2.),

View File

@ -70,7 +70,7 @@ fn universal_button_click_behavior(
mut trigger: Trigger<Pointer<Click>>,
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) {
// This would be a great place to play a little sound effect too!
color.0 = PRESSED_BUTTON.into();

View File

@ -26,20 +26,20 @@ fn setup(mut commands: Commands) {
commands
.spawn(Node {
flex_direction: FlexDirection::Column,
row_gap: Val::Px(30.),
margin: UiRect::all(Val::Px(30.)),
row_gap: Val::Px(20.),
margin: UiRect::all(Val::Px(20.)),
..Default::default()
})
.with_children(|commands| {
for (b, stops) in [
(
5.,
4.,
vec![
ColorStop::new(Color::WHITE, Val::Percent(15.)),
ColorStop::new(Color::BLACK, Val::Percent(85.)),
],
),
(5., vec![RED.into(), BLUE.into(), LIME.into()]),
(4., vec![RED.into(), BLUE.into(), LIME.into()]),
(
0.,
vec![
@ -64,11 +64,11 @@ fn setup(mut commands: Commands) {
commands
.spawn(Node {
flex_direction: FlexDirection::Column,
row_gap: Val::Px(10.),
row_gap: Val::Px(5.),
..Default::default()
})
.with_children(|commands| {
for (w, h) in [(100., 100.), (50., 100.), (100., 50.)] {
for (w, h) in [(70., 70.), (35., 70.), (70., 35.)] {
commands
.spawn(Node {
column_gap: Val::Px(10.),
@ -108,7 +108,7 @@ fn setup(mut commands: Commands) {
aspect_ratio: Some(1.),
height: Val::Percent(100.),
border: UiRect::all(Val::Px(b)),
margin: UiRect::left(Val::Px(30.)),
margin: UiRect::left(Val::Px(20.)),
..default()
},
BorderRadius::all(Val::Px(20.)),
@ -128,7 +128,7 @@ fn setup(mut commands: Commands) {
aspect_ratio: Some(1.),
height: Val::Percent(100.),
border: UiRect::all(Val::Px(b)),
margin: UiRect::left(Val::Px(30.)),
margin: UiRect::left(Val::Px(20.)),
..default()
},
BorderRadius::all(Val::Px(20.)),
@ -148,7 +148,7 @@ fn setup(mut commands: Commands) {
aspect_ratio: Some(1.),
height: Val::Percent(100.),
border: UiRect::all(Val::Px(b)),
margin: UiRect::left(Val::Px(30.)),
margin: UiRect::left(Val::Px(20.)),
..default()
},
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};
const CONTAINER_SIZE: f32 = 150.0;
const HALF_CONTAINER_SIZE: f32 = CONTAINER_SIZE / 2.0;
const LOOP_LENGTH: f32 = 4.0;
fn main() {
@ -41,16 +40,16 @@ struct AnimationState {
struct Container(u8);
trait UpdateTransform {
fn update(&self, t: f32, transform: &mut Transform);
fn update(&self, t: f32, transform: &mut UiTransform);
}
#[derive(Component)]
struct Move;
impl UpdateTransform for Move {
fn update(&self, t: f32, transform: &mut Transform) {
transform.translation.x = ops::sin(t * TAU - FRAC_PI_2) * HALF_CONTAINER_SIZE;
transform.translation.y = -ops::cos(t * TAU - FRAC_PI_2) * HALF_CONTAINER_SIZE;
fn update(&self, t: f32, transform: &mut UiTransform) {
transform.translation.x = Val::Percent(ops::sin(t * TAU - FRAC_PI_2) * 50.);
transform.translation.y = Val::Percent(-ops::cos(t * TAU - FRAC_PI_2) * 50.);
}
}
@ -58,7 +57,7 @@ impl UpdateTransform for Move {
struct 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.y = 1.0 + 0.5 * ops::cos(t * TAU + PI).max(0.0);
}
@ -68,9 +67,8 @@ impl UpdateTransform for Scale {
struct Rotate;
impl UpdateTransform for Rotate {
fn update(&self, t: f32, transform: &mut Transform) {
transform.rotation =
Quat::from_axis_angle(Vec3::Z, (ops::cos(t * TAU) * 45.0).to_radians());
fn update(&self, t: f32, transform: &mut UiTransform) {
transform.rotation = Rot2::radians(ops::cos(t * TAU) * 45.0);
}
}
@ -175,10 +173,6 @@ fn spawn_container(
update_transform: impl UpdateTransform + Component,
spawn_children: impl FnOnce(&mut ChildSpawnerCommands),
) {
let mut transform = Transform::default();
update_transform.update(0.0, &mut transform);
parent
.spawn((
Node {
@ -198,11 +192,8 @@ fn spawn_container(
Node {
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
top: Val::Px(transform.translation.x),
left: Val::Px(transform.translation.y),
..default()
},
transform,
update_transform,
))
.with_children(spawn_children);
@ -233,13 +224,10 @@ fn update_animation(
fn update_transform<T: UpdateTransform + Component>(
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);
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()
};
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)
} else {
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
| {
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(
|mut trigger: Trigger<Pointer<Click>>,
mut focus: ResMut<InputFocus>| {
focus.0 = Some(trigger.target());
focus.0 = Some(trigger.target().unwrap());
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>) {
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) {
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>) {
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_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.