Merge branch 'bevyengine:main' into schema-types-metadata
This commit is contained in:
commit
7beca6fd0e
11
Cargo.toml
11
Cargo.toml
@ -3543,6 +3543,17 @@ description = "Illustrates how to use 9 Slicing for TextureAtlases in UI"
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "ui_transform"
|
||||
path = "examples/ui/ui_transform.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.ui_transform]
|
||||
name = "UI Transform"
|
||||
description = "An example demonstrating how to translate, rotate and scale UI elements."
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "viewport_debug"
|
||||
path = "examples/ui/viewport_debug.rs"
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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,
|
||||
/// };
|
||||
///
|
||||
/// #[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 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
|
||||
/// }
|
||||
///
|
||||
/// 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,
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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| {
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1418,9 +1418,8 @@ 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) {
|
||||
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
|
||||
@ -1440,7 +1439,6 @@ impl ScheduleGraph {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conflicting_systems
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
///
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,7 +769,6 @@ impl<'w> DeferredWorld<'w> {
|
||||
) where
|
||||
T: Traversal<E>,
|
||||
{
|
||||
loop {
|
||||
Observers::invoke::<_>(
|
||||
self.reborrow(),
|
||||
event,
|
||||
@ -779,8 +778,11 @@ impl<'w> DeferredWorld<'w> {
|
||||
&mut propagate,
|
||||
caller,
|
||||
);
|
||||
let Some(mut target) = target else { return };
|
||||
|
||||
loop {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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];
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
};
|
||||
},
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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()),
|
||||
|
||||
@ -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 π
|
||||
|
||||
@ -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],
|
||||
});
|
||||
}
|
||||
|
||||
@ -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 π
|
||||
|
||||
@ -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 π
|
||||
|
||||
@ -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
|
||||
)]
|
||||
|
||||
191
crates/bevy_ui/src/ui_transform.rs
Normal file
191
crates/bevy_ui/src/ui_transform.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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`].
|
||||
//
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
))));
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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!
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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.);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.),
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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.)),
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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
302
examples/ui/ui_transform.rs
Normal 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),)]
|
||||
),
|
||||
]
|
||||
)
|
||||
]
|
||||
)],
|
||||
));
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
12
release-content/migration-guides/observer_triggers.md
Normal file
12
release-content/migration-guides/observer_triggers.md
Normal 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.
|
||||
@ -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.
|
||||
32
release-content/migration-guides/specialized_ui_transform.md
Normal file
32
release-content/migration-guides/specialized_ui_transform.md
Normal 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.
|
||||
@ -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
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@ -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.
|
||||
Loading…
Reference in New Issue
Block a user