diff --git a/Cargo.toml b/Cargo.toml index 0809d16c0fbd3..258bd532e927b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,6 +147,7 @@ default = [ "bevy_picking", "bevy_render", "bevy_scene", + "bevy_scene2", "bevy_image", "bevy_mesh", "bevy_camera", @@ -178,6 +179,7 @@ default = [ "wayland", "debug", "zstd_rust", + "experimental_bevy_feathers", ] # Recommended defaults for no_std applications @@ -240,6 +242,9 @@ bevy_render = ["bevy_internal/bevy_render"] # Provides scene functionality bevy_scene = ["bevy_internal/bevy_scene"] +# Provides scene functionality +bevy_scene2 = ["bevy_internal/bevy_scene2", "bevy_asset"] + # Provides raytraced lighting (experimental) bevy_solari = ["bevy_internal/bevy_solari"] @@ -612,6 +617,7 @@ bevy_image = { path = "crates/bevy_image", version = "0.17.1", default-features bevy_reflect = { path = "crates/bevy_reflect", version = "0.17.1", default-features = false } bevy_render = { path = "crates/bevy_render", version = "0.17.1", default-features = false } bevy_state = { path = "crates/bevy_state", version = "0.17.1", default-features = false } +bevy_scene2 = { path = "crates/bevy_scene2", version = "0.17.1", default-features = false } # Needed to poll Task examples futures-lite = "2.0.1" futures-timer = { version = "3", features = ["wasm-bindgen", "gloo-timers"] } @@ -622,6 +628,7 @@ event-listener = "5.3.0" anyhow = "1" accesskit = "0.21" nonmax = "0.5" +variadics_please = "1" [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] ureq = { version = "3.0.8", features = ["json"] } @@ -2824,6 +2831,14 @@ description = "Demonstrates loading from and saving scenes to files" category = "Scene" wasm = false +[[example]] +name = "bsn" +path = "examples/scene/bsn.rs" + +[[example]] +name = "ui_scene" +path = "examples/scene/ui_scene.rs" + # Shaders [[package.metadata.example_category]] name = "Shaders" diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs index f11e1fb51a8a1..4c9ef416a3106 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -222,7 +222,7 @@ impl ManageAccessibilityUpdates { /// /// This behavior may or may not be intended, so please utilize /// `AccessibilityNode`s with care. -#[derive(Component, Clone, Deref, DerefMut)] +#[derive(Component, Clone, Deref, DerefMut, Default)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct AccessibilityNode( /// A representation of this component's entity to `AccessKit`. diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index 159e689ccf26c..0150f27331d2d 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -131,10 +131,16 @@ pub struct AnimationGraph { } /// A [`Handle`] to the [`AnimationGraph`] to be used by the [`AnimationPlayer`](crate::AnimationPlayer) on the same entity. -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Clone)] pub struct AnimationGraphHandle(pub Handle); +impl Default for AnimationGraphHandle { + fn default() -> Self { + Self(Handle::default()) + } +} + impl From for AssetId { fn from(handle: AnimationGraphHandle) -> Self { handle.id() diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index 8924bf87de850..4d22e938b2576 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -1,9 +1,14 @@ use crate::{ - meta::MetaTransform, Asset, AssetId, AssetIndexAllocator, AssetPath, InternalAssetId, - UntypedAssetId, + meta::MetaTransform, Asset, AssetId, AssetIndexAllocator, AssetPath, AssetServer, + InternalAssetId, UntypedAssetId, }; use alloc::sync::Arc; -use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath}; +use bevy_ecs::{ + error::Result, + template::{GetTemplate, Template, TemplateContext}, +}; +use bevy_platform::collections::Equivalent; +use bevy_reflect::{Reflect, TypePath}; use core::{ any::TypeId, hash::{Hash, Hasher}, @@ -130,7 +135,7 @@ impl core::fmt::Debug for StrongHandle { /// /// [`Handle::Strong`], via [`StrongHandle`] also provides access to useful [`Asset`] metadata, such as the [`AssetPath`] (if it exists). #[derive(Reflect)] -#[reflect(Default, Debug, Hash, PartialEq, Clone)] +#[reflect(Debug, Hash, PartialEq, Clone)] pub enum Handle { /// A "strong" reference to a live (or loading) [`Asset`]. If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept /// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata. @@ -150,6 +155,9 @@ impl Clone for Handle { } impl Handle { + pub fn default() -> Self { + Handle::Uuid(AssetId::::DEFAULT_UUID, PhantomData) + } /// Returns the [`AssetId`] of this [`Asset`]. #[inline] pub fn id(&self) -> AssetId { @@ -189,9 +197,37 @@ impl Handle { } } -impl Default for Handle { +impl GetTemplate for Handle { + type Template = HandleTemplate; +} + +pub struct HandleTemplate { + path: AssetPath<'static>, + marker: PhantomData, +} + +impl Default for HandleTemplate { fn default() -> Self { - Handle::Uuid(AssetId::::DEFAULT_UUID, PhantomData) + Self { + path: Default::default(), + marker: Default::default(), + } + } +} + +impl>, T> From for HandleTemplate { + fn from(value: I) -> Self { + Self { + path: value.into(), + marker: PhantomData, + } + } +} + +impl Template for HandleTemplate { + type Output = Handle; + fn build(&mut self, context: &mut TemplateContext) -> Result> { + Ok(context.resource::().load(&self.path)) } } @@ -219,6 +255,13 @@ impl Hash for Handle { } } +// Handle uses AssetId when hashing. This enables using AssetId instead of handle with hashsets and hashmaps. +impl Equivalent> for AssetId { + fn equivalent(&self, key: &Handle) -> bool { + *self == key.id() + } +} + impl PartialOrd for Handle { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 0b957c59172e3..cd9a8fab8e2a2 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -473,6 +473,12 @@ impl VisitAssetDependencies for Option> { } } +impl VisitAssetDependencies for UntypedAssetId { + fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + visit(*self); + } +} + impl VisitAssetDependencies for UntypedHandle { fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { visit(self.id()); @@ -503,34 +509,18 @@ impl VisitAssetDependencies for [UntypedHandle; N] { } } -impl VisitAssetDependencies for Vec> { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { - for dependency in self { - visit(dependency.id().untyped()); - } - } -} - -impl VisitAssetDependencies for Vec { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { - for dependency in self { - visit(dependency.id()); - } - } -} - -impl VisitAssetDependencies for HashSet> { +impl VisitAssetDependencies for Vec { fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { for dependency in self { - visit(dependency.id().untyped()); + dependency.visit_dependencies(visit); } } } -impl VisitAssetDependencies for HashSet { +impl VisitAssetDependencies for HashSet { fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { for dependency in self { - visit(dependency.id()); + dependency.visit_dependencies(visit); } } } diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 3db044029c36f..708c7f8a5b110 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -873,6 +873,20 @@ impl AssetServer { self.load_asset(LoadedAsset::new_with_dependencies(asset)) } + // TODO: this is a hack: this allows the asset to pretend to be from the path, but this will cause issues in practice + #[must_use = "not using the returned strong handle may result in the unexpected release of the asset"] + pub fn load_with_path<'a, A: Asset>( + &self, + path: impl Into>, + asset: A, + ) -> Handle { + let loaded_asset: LoadedAsset = asset.into(); + let erased_loaded_asset: ErasedLoadedAsset = loaded_asset.into(); + let path: AssetPath = path.into(); + self.load_asset_untyped(Some(path.into_owned()), erased_loaded_asset) + .typed_debug_checked() + } + pub(crate) fn load_asset(&self, asset: impl Into>) -> Handle { let loaded_asset: LoadedAsset = asset.into(); let erased_loaded_asset: ErasedLoadedAsset = loaded_asset.into(); diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index bde327b229e85..e5ceef1b687a6 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -122,6 +122,7 @@ log = { version = "0.4", default-features = false } bumpalo = "3" subsecond = { version = "0.7.0-rc.0", optional = true } slotmap = { version = "1.0.7", default-features = false } +downcast-rs = { version = "2", default-features = false, features = ["std"] } concurrent-queue = { version = "2.5.0", default-features = false } [target.'cfg(not(all(target_has_atomic = "8", target_has_atomic = "16", target_has_atomic = "32", target_has_atomic = "64", target_has_atomic = "ptr")))'.dependencies] diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index b8098965a3697..ca75db3469c2e 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -9,6 +9,8 @@ mod event; mod message; mod query_data; mod query_filter; +mod template; +mod variant_defaults; mod world_query; use crate::{ @@ -754,3 +756,15 @@ pub fn derive_from_world(input: TokenStream) -> TokenStream { } }) } + +/// Derives GetTemplate. +#[proc_macro_derive(GetTemplate, attributes(template, default))] +pub fn derive_get_template(input: TokenStream) -> TokenStream { + template::derive_get_template(input) +} + +/// Derives VariantDefaults. +#[proc_macro_derive(VariantDefaults)] +pub fn derive_variant_defaults(input: TokenStream) -> TokenStream { + variant_defaults::derive_variant_defaults(input) +} diff --git a/crates/bevy_ecs/macros/src/template.rs b/crates/bevy_ecs/macros/src/template.rs new file mode 100644 index 0000000000000..60ef4a1e323ac --- /dev/null +++ b/crates/bevy_ecs/macros/src/template.rs @@ -0,0 +1,367 @@ +use bevy_macro_utils::BevyManifest; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, Data, DeriveInput, Fields, FieldsUnnamed, Index, Path}; + +const TEMPLATE_ATTRIBUTE: &str = "template"; +const TEMPLATE_DEFAULT_ATTRIBUTE: &str = "default"; + +pub(crate) fn derive_get_template(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let manifest = BevyManifest::shared(); + let bevy_ecs = manifest.get_path("bevy_ecs"); + + let type_ident = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + let template_ident = format_ident!("{type_ident}Template"); + + let is_pub = matches!(ast.vis, syn::Visibility::Public(_)); + let maybe_pub = if is_pub { quote!(pub) } else { quote!() }; + + let template = match &ast.data { + Data::Struct(data_struct) => { + let StructImpl { + template_fields, + template_field_builds, + template_field_defaults, + .. + } = struct_impl(&data_struct.fields, &bevy_ecs, false); + match &data_struct.fields { + Fields::Named(_) => { + quote! { + #[allow(missing_docs)] + #maybe_pub struct #template_ident #impl_generics #where_clause { + #(#template_fields,)* + } + + impl #impl_generics #bevy_ecs::template::Template for #template_ident #type_generics #where_clause { + type Output = #type_ident #type_generics; + fn build(&mut self, context: &mut #bevy_ecs::template::TemplateContext) -> #bevy_ecs::error::Result { + Ok(#type_ident { + #(#template_field_builds,)* + }) + } + } + + impl #impl_generics Default for #template_ident #type_generics #where_clause { + fn default() -> Self { + Self { + #(#template_field_defaults,)* + } + } + } + } + } + Fields::Unnamed(_) => { + quote! { + #[allow(missing_docs)] + #maybe_pub struct #template_ident #impl_generics ( + #(#template_fields,)* + ) #where_clause; + + impl #impl_generics #bevy_ecs::template::Template for #template_ident #type_generics #where_clause { + type Output = #type_ident #type_generics; + fn build(&mut self, context: &mut #bevy_ecs::template::TemplateContext) -> #bevy_ecs::error::Result { + Ok(#type_ident ( + #(#template_field_builds,)* + )) + } + } + + impl #impl_generics Default for #template_ident #type_generics #where_clause { + fn default() -> Self { + Self ( + #(#template_field_defaults,)* + ) + } + } + } + } + Fields::Unit => { + quote! { + #[allow(missing_docs)] + #maybe_pub struct #template_ident; + + impl #impl_generics #bevy_ecs::template::Template for #template_ident #type_generics #where_clause { + type Output = #type_ident; + fn build(&mut self, context: &mut #bevy_ecs::template::TemplateContext) -> #bevy_ecs::error::Result { + Ok(#type_ident) + } + } + + impl #impl_generics Default for #template_ident #type_generics #where_clause { + fn default() -> Self { + Self + } + } + } + } + } + } + Data::Enum(data_enum) => { + let mut variant_definitions = Vec::new(); + let mut variant_builds = Vec::new(); + let mut variant_default_ident = None; + let mut variant_defaults = Vec::new(); + for variant in &data_enum.variants { + let StructImpl { + template_fields, + template_field_builds, + template_field_defaults, + .. + } = struct_impl(&variant.fields, &bevy_ecs, true); + + let is_default = variant + .attrs + .iter() + .find(|a| a.path().is_ident(TEMPLATE_DEFAULT_ATTRIBUTE)) + .is_some(); + if is_default { + if variant_default_ident.is_some() { + panic!("Cannot have multiple default variants"); + } + } + let variant_ident = &variant.ident; + let variant_name_lower = variant_ident.to_string().to_lowercase(); + let variant_default_name = format_ident!("default_{}", variant_name_lower); + match &variant.fields { + Fields::Named(fields) => { + variant_definitions.push(quote! { + #variant_ident { + #(#template_fields,)* + } + }); + let field_idents = fields.named.iter().map(|f| &f.ident); + variant_builds.push(quote! { + // TODO: proper assignments here + #template_ident::#variant_ident { + #(#field_idents,)* + } => { + #type_ident::#variant_ident { + #(#template_field_builds,)* + } + } + }); + + if is_default { + variant_default_ident = Some(quote! { + Self::#variant_ident { + #(#template_field_defaults,)* + } + }); + } + variant_defaults.push(quote! { + #maybe_pub fn #variant_default_name() -> Self { + Self::#variant_ident { + #(#template_field_defaults,)* + } + } + }) + } + Fields::Unnamed(FieldsUnnamed { unnamed: f, .. }) => { + let field_idents = + f.iter().enumerate().map(|(i, _)| format_ident!("t{}", i)); + variant_definitions.push(quote! { + #variant_ident(#(#template_fields,)*) + }); + variant_builds.push(quote! { + // TODO: proper assignments here + #template_ident::#variant_ident( + #(#field_idents,)* + ) => { + #type_ident::#variant_ident( + #(#template_field_builds,)* + ) + } + }); + if is_default { + variant_default_ident = Some(quote! { + Self::#variant_ident( + #(#template_field_defaults,)* + ) + }); + } + + variant_defaults.push(quote! { + #maybe_pub fn #variant_default_name() -> Self { + Self::#variant_ident( + #(#template_field_defaults,)* + ) + } + }) + } + Fields::Unit => { + variant_definitions.push(quote! {#variant_ident}); + variant_builds.push( + quote! {#template_ident::#variant_ident => #type_ident::#variant_ident}, + ); + if is_default { + variant_default_ident = Some(quote! { + Self::#variant_ident + }); + } + variant_defaults.push(quote! { + #maybe_pub fn #variant_default_name() -> Self { + Self::#variant_ident + } + }) + } + } + } + + if variant_default_ident.is_none() { + panic!("Deriving Template for enums requires picking a default variant using #[default]"); + } + + quote! { + #[allow(missing_docs)] + #maybe_pub enum #template_ident #type_generics #where_clause { + #(#variant_definitions,)* + } + + impl #impl_generics #template_ident #type_generics #where_clause { + #(#variant_defaults)* + } + + impl #impl_generics #bevy_ecs::template::Template for #template_ident #type_generics #where_clause { + type Output = #type_ident #type_generics; + fn build(&mut self, context: &mut #bevy_ecs::template::TemplateContext) -> #bevy_ecs::error::Result { + Ok(match self { + #(#variant_builds,)* + }) + } + } + + impl #impl_generics Default for #template_ident #type_generics #where_clause { + fn default() -> Self { + #variant_default_ident + } + } + } + } + Data::Union(_) => panic!("Union types are not supported yet."), + }; + + TokenStream::from(quote! { + impl #impl_generics #bevy_ecs::template::GetTemplate for #type_ident #type_generics #where_clause { + type Template = #template_ident #type_generics; + } + + #template + }) +} + +struct StructImpl { + template_fields: Vec, + template_field_builds: Vec, + template_field_defaults: Vec, +} + +fn struct_impl(fields: &Fields, bevy_ecs: &Path, is_enum: bool) -> StructImpl { + let mut template_fields = Vec::with_capacity(fields.len()); + let mut template_field_builds = Vec::with_capacity(fields.len()); + let mut template_field_defaults = Vec::with_capacity(fields.len()); + let is_named = matches!(fields, Fields::Named(_)); + for (index, field) in fields.iter().enumerate() { + let is_template = field + .attrs + .iter() + .find(|a| a.path().is_ident(TEMPLATE_ATTRIBUTE)) + .is_some(); + let is_pub = matches!(field.vis, syn::Visibility::Public(_)); + let field_maybe_pub = if is_pub { quote!(pub) } else { quote!() }; + let ident = &field.ident; + let ty = &field.ty; + let index = Index::from(index); + if is_named { + if is_template { + template_fields.push(quote! { + #field_maybe_pub #ident: #bevy_ecs::template::TemplateField<<#ty as #bevy_ecs::template::GetTemplate>::Template> + }); + if is_enum { + template_field_builds.push(quote! { + #ident: match #ident { + #bevy_ecs::template::TemplateField::Template(template) => template.build(context)?, + #bevy_ecs::template::TemplateField::Value(value) => Clone::clone(value), + } + }); + } else { + template_field_builds.push(quote! { + #ident: match &mut self.#ident { + #bevy_ecs::template::TemplateField::Template(template) => template.build(context)?, + #bevy_ecs::template::TemplateField::Value(value) => Clone::clone(value), + } + }); + } + template_field_defaults.push(quote! { + #ident: Default::default() + }); + } else { + template_fields.push(quote! { + #field_maybe_pub #ident: <#ty as #bevy_ecs::template::GetTemplate>::Template + }); + if is_enum { + template_field_builds.push(quote! { + #ident: #ident.build(context)? + }); + } else { + template_field_builds.push(quote! { + #ident: self.#ident.build(context)? + }); + } + + template_field_defaults.push(quote! { + #ident: Default::default() + }); + } + } else { + if is_template { + template_fields.push(quote! { + #field_maybe_pub #bevy_ecs::template::TemplateField<<#ty as #bevy_ecs::template::GetTemplate>::Template> + }); + if is_enum { + let enum_tuple_ident = format_ident!("t{}", index); + template_field_builds.push(quote! { + match #enum_tuple_ident { + #bevy_ecs::template::TemplateField::Template(template) => template.build(context)?, + #bevy_ecs::template::TemplateField::Value(value) => Clone::clone(value), + } + }); + } else { + template_field_builds.push(quote! { + match &mut self.#index { + #bevy_ecs::template::TemplateField::Template(template) => template.build(context)?, + #bevy_ecs::template::TemplateField::Value(value) => Clone::clone(value), + } + }); + } + template_field_defaults.push(quote! { + Default::default() + }); + } else { + template_fields.push(quote! { + #field_maybe_pub <#ty as #bevy_ecs::template::GetTemplate>::Template + }); + if is_enum { + let enum_tuple_ident = format_ident!("t{}", index); + template_field_builds.push(quote! { + #enum_tuple_ident.build(context)? + }); + } else { + template_field_builds.push(quote! { + self.#index.build(context)? + }); + } + template_field_defaults.push(quote! { + Default::default() + }); + } + } + } + StructImpl { + template_fields, + template_field_builds, + template_field_defaults, + } +} diff --git a/crates/bevy_ecs/macros/src/variant_defaults.rs b/crates/bevy_ecs/macros/src/variant_defaults.rs new file mode 100644 index 0000000000000..0429c8a64caab --- /dev/null +++ b/crates/bevy_ecs/macros/src/variant_defaults.rs @@ -0,0 +1,58 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, Data, DeriveInput}; + +pub(crate) fn derive_variant_defaults(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let type_ident = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + let Data::Enum(data_enum) = &ast.data else { + panic!("Can only derive VariantDefaults for enums"); + }; + + let mut variant_defaults = Vec::new(); + for variant in &data_enum.variants { + let variant_ident = &variant.ident; + let variant_name_lower = variant_ident.to_string().to_lowercase(); + let variant_default_name = format_ident!("default_{}", variant_name_lower); + match &variant.fields { + syn::Fields::Named(fields_named) => { + let fields = fields_named.named.iter().map(|f| &f.ident); + variant_defaults.push(quote! { + #[allow(missing_docs)] + pub fn #variant_default_name() -> Self { + Self::#variant_ident { + #(#fields: Default::default(),)* + } + } + }) + } + syn::Fields::Unnamed(fields_unnamed) => { + let fields = fields_unnamed + .unnamed + .iter() + .map(|_| quote! {Default::default()}); + variant_defaults.push(quote! { + #[allow(missing_docs)] + pub fn #variant_default_name() -> Self { + Self::#variant_ident( + #(#fields,)* + ) + } + }) + } + syn::Fields::Unit => variant_defaults.push(quote! { + #[allow(missing_docs)] + pub fn #variant_default_name() -> Self { + Self::#variant_ident + } + }), + } + } + + TokenStream::from(quote! { + impl #impl_generics #type_ident #type_generics #where_clause { + #(#variant_defaults)* + } + }) +} diff --git a/crates/bevy_ecs/src/entity/entity_path.rs b/crates/bevy_ecs/src/entity/entity_path.rs new file mode 100644 index 0000000000000..c494c1db41a2f --- /dev/null +++ b/crates/bevy_ecs/src/entity/entity_path.rs @@ -0,0 +1,51 @@ +use crate::{entity::Entity, world::EntityWorldMut}; +use alloc::{borrow::Cow, string::String}; +use log::warn; +use thiserror::Error; + +/// A path to an entity. +pub struct EntityPath<'a>(Cow<'a, str>); + +impl<'a> Default for EntityPath<'a> { + fn default() -> Self { + Self(Default::default()) + } +} + +impl<'a> From<&'a str> for EntityPath<'a> { + #[inline] + fn from(entity_path: &'a str) -> Self { + EntityPath(Cow::Borrowed(entity_path)) + } +} + +impl<'a> From<&'a String> for EntityPath<'a> { + #[inline] + fn from(entity_path: &'a String) -> Self { + EntityPath(Cow::Borrowed(entity_path.as_str())) + } +} + +impl From for EntityPath<'static> { + #[inline] + fn from(asset_path: String) -> Self { + EntityPath(Cow::Owned(asset_path.into())) + } +} + +/// An [`Error`] that occurs when failing to resolve an [`EntityPath`]. +#[derive(Error, Debug)] +pub enum ResolveEntityPathError {} + +impl<'w> EntityWorldMut<'w> { + /// Attempt to resolve the given `path` to an [`Entity`]. + pub fn resolve_path<'a>( + &self, + path: &EntityPath<'a>, + ) -> Result { + if !path.0.is_empty() { + warn!("Resolving non-empty entity paths doesn't work yet!"); + } + Ok(self.id()) + } +} diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 3e0ee0a813492..adc3510168718 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -37,6 +37,7 @@ //! [`EntityWorldMut::remove`]: crate::world::EntityWorldMut::remove mod clone_entities; +mod entity_path; mod entity_set; mod map_entities; #[cfg(feature = "bevy_reflect")] @@ -45,7 +46,7 @@ use bevy_reflect::Reflect; use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; pub use clone_entities::*; -use derive_more::derive::Display; +pub use entity_path::*; pub use entity_set::*; pub use map_entities::*; @@ -68,7 +69,6 @@ pub mod unique_array; pub mod unique_slice; pub mod unique_vec; -use nonmax::NonMaxU32; pub use unique_array::{UniqueEntityArray, UniqueEntityEquivalentArray}; pub use unique_slice::{UniqueEntityEquivalentSlice, UniqueEntitySlice}; pub use unique_vec::{UniqueEntityEquivalentVec, UniqueEntityVec}; @@ -82,7 +82,9 @@ use crate::{ use alloc::vec::Vec; use bevy_platform::sync::atomic::Ordering; use core::{fmt, hash::Hash, mem, num::NonZero, panic::Location}; +use derive_more::derive::Display; use log::warn; +use nonmax::NonMaxU32; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 2e9174bf2404c..e4c86b71bb170 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -55,6 +55,7 @@ pub mod schedule; pub mod spawn; pub mod storage; pub mod system; +pub mod template; pub mod traversal; pub mod world; @@ -103,6 +104,7 @@ pub mod prelude { Res, ResMut, Single, System, SystemIn, SystemInput, SystemParamBuilder, SystemParamFunction, }, + template::{GetTemplate, Template}, world::{ EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, FromWorld, World, @@ -124,6 +126,8 @@ pub mod prelude { pub use crate::reflect::AppFunctionRegistry; } +pub use bevy_ecs_macros::VariantDefaults; + /// Exports used by macros. /// /// These are not meant to be used directly and are subject to breaking changes. diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index 317c8f5017bb5..6ae64e173f373 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -6,9 +6,9 @@ use alloc::{ borrow::{Cow, ToOwned}, string::String, }; -use bevy_platform::hash::FixedHasher; +use bevy_platform::hash::Hashed; use core::{ - hash::{BuildHasher, Hash, Hasher}, + hash::{Hash, Hasher}, ops::Deref, }; @@ -47,10 +47,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; all(feature = "serialize", feature = "bevy_reflect"), reflect(Deserialize, Serialize) )] -pub struct Name { - hash: u64, // Won't be serialized - name: Cow<'static, str>, -} +pub struct Name(pub HashedStr); impl Default for Name { fn default() -> Self { @@ -58,15 +55,28 @@ impl Default for Name { } } +/// A wrapper over Hashed. This exists to make Name("value".into()) possible, which plays nicely with contexts like the `bsn!` macro. +#[derive(Reflect, Clone)] +pub struct HashedStr(Hashed>); + +impl From<&'static str> for HashedStr { + fn from(value: &'static str) -> Self { + Self(Hashed::new(Cow::Borrowed(value))) + } +} + +impl From for HashedStr { + fn from(value: String) -> Self { + Self(Hashed::new(Cow::Owned(value))) + } +} + impl Name { /// Creates a new [`Name`] from any string-like type. /// /// The internal hash will be computed immediately. pub fn new(name: impl Into>) -> Self { - let name = name.into(); - let mut name = Name { name, hash: 0 }; - name.update_hash(); - name + Self(HashedStr(Hashed::new(name.into()))) } /// Sets the entity's name. @@ -82,33 +92,28 @@ impl Name { /// This will allocate a new string if the name was previously /// created from a borrow. #[inline(always)] - pub fn mutate(&mut self, f: F) { - f(self.name.to_mut()); - self.update_hash(); + pub fn mutate(&mut self, _f: F) { + todo!("Expose this functionality in Hashed") } /// Gets the name of the entity as a `&str`. #[inline(always)] pub fn as_str(&self) -> &str { - &self.name - } - - fn update_hash(&mut self) { - self.hash = FixedHasher.hash_one(&self.name); + &self.0 .0 } } impl core::fmt::Display for Name { #[inline(always)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::Display::fmt(&self.name, f) + core::fmt::Display::fmt(&*self.0 .0, f) } } impl core::fmt::Debug for Name { #[inline(always)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::Debug::fmt(&self.name, f) + core::fmt::Debug::fmt(&self.0 .0, f) } } @@ -172,7 +177,7 @@ impl From for Name { impl AsRef for Name { #[inline(always)] fn as_ref(&self) -> &str { - &self.name + &self.0 .0 } } @@ -186,24 +191,24 @@ impl From<&Name> for String { impl From for String { #[inline(always)] fn from(val: Name) -> String { - val.name.into_owned() + val.as_str().to_owned() } } impl Hash for Name { fn hash(&self, state: &mut H) { - self.name.hash(state); + Hash::hash(&self.0 .0, state); } } impl PartialEq for Name { fn eq(&self, other: &Self) -> bool { - if self.hash != other.hash { + if self.0 .0.hash() != other.0 .0.hash() { // Makes the common case of two strings not been equal very fast return false; } - self.name.eq(&other.name) + self.0 .0.eq(&other.0 .0) } } @@ -217,7 +222,7 @@ impl PartialOrd for Name { impl Ord for Name { fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.name.cmp(&other.name) + self.0 .0.cmp(&other.0 .0) } } @@ -225,7 +230,7 @@ impl Deref for Name { type Target = str; fn deref(&self) -> &Self::Target { - self.name.as_ref() + self.as_str() } } diff --git a/crates/bevy_ecs/src/template.rs b/crates/bevy_ecs/src/template.rs new file mode 100644 index 0000000000000..730513bb61644 --- /dev/null +++ b/crates/bevy_ecs/src/template.rs @@ -0,0 +1,357 @@ +//! Functionality that relates to the [`Template`] trait. + +pub use bevy_ecs_macros::GetTemplate; + +use crate::{ + bundle::Bundle, + entity::{Entity, EntityPath}, + error::{BevyError, Result}, + resource::Resource, + world::{EntityWorldMut, World}, +}; +use alloc::{boxed::Box, vec, vec::Vec}; +use bevy_platform::collections::hash_map::Entry; +use bevy_utils::TypeIdMap; +use core::any::{Any, TypeId}; +use downcast_rs::{impl_downcast, Downcast}; +use variadics_please::all_tuples; + +/// A [`Template`] is something that, given a spawn context (target [`Entity`], [`World`](crate::world::World), etc), can produce a [`Template::Output`]. +pub trait Template { + /// The type of value produced by this [`Template`]. + type Output; + + /// Uses this template and the given `entity` context to produce a [`Template::Output`]. + fn build(&mut self, context: &mut TemplateContext) -> Result; + + /// This is used to register information about the template, such as dependencies that should be loaded before it is instantiated. + #[inline] + fn register_data(&self, _data: &mut TemplateData) {} +} + +/// The context used to apply the current [`Template`]. This contains a reference to the entity that the template is being +/// applied to. +pub struct TemplateContext<'a> { + /// The current entity the template is being applied to + pub entity: &'a mut EntityWorldMut<'a>, + /// The scoped entities mapping for the current template context + pub scoped_entities: &'a mut ScopedEntities, + /// The entity scopes for the current template context. This matches + /// the `scoped_entities`. + pub entity_scopes: &'a EntityScopes, +} + +impl<'a> TemplateContext<'a> { + /// Creates a new [`TemplateContext`]. + pub fn new( + entity: &'a mut EntityWorldMut<'a>, + scoped_entities: &'a mut ScopedEntities, + entity_scopes: &'a EntityScopes, + ) -> Self { + Self { + entity, + scoped_entities, + entity_scopes, + } + } + + /// Retrieves the scoped entity if it has already been spawned, and spawns a new entity if it has not + /// yet been spawned. + pub fn get_scoped_entity(&mut self, scope: usize, index: usize) -> Entity { + self.scoped_entities.get( + // SAFETY: this only uses the world to spawn an empty entity + unsafe { self.entity.world_mut() }, + self.entity_scopes, + scope, + index, + ) + } +} + +/// A mapping from from an entity reference's (scope, index) to a contiguous flat index that uniquely +/// identifies the entity within a scene. +#[derive(Default, Debug)] +pub struct EntityScopes { + scopes: Vec>>, + next_index: usize, +} + +impl EntityScopes { + /// The number of entities defined across all scopes. + #[inline] + pub fn entity_count(&self) -> usize { + self.next_index + } + + /// Allocate a new contiguous entity index for the given (scope, index) pair. + pub fn alloc(&mut self, scope: usize, index: usize) { + *self.get_mut(scope, index) = Some(self.next_index); + self.next_index += 1; + } + + /// Assign an existing contiguous entity index for the given (scope, index) pair. + /// This is generally used when there are multiple (scope, index) pairs that point + /// to the same entity (ex: scene inheritance). + pub fn assign(&mut self, scope: usize, index: usize, value: usize) { + let option = self.get_mut(scope, index); + *option = Some(value); + } + + #[allow(unsafe_code)] + fn get_mut(&mut self, scope: usize, index: usize) -> &mut Option { + // NOTE: this is ok because PatchContext::new_scope adds scopes as they are created. + // this shouldn't panic unless internals are broken. + let indices = &mut self.scopes[scope]; + if index >= indices.len() { + indices.resize_with(index + 1, || None); + } + // SAFETY: just allocated above + unsafe { indices.get_unchecked_mut(index) } + } + + /// Gets the assigned contiguous entity index for the given (scope, index) pair + pub fn get(&self, scope: usize, index: usize) -> Option { + *self.scopes.get(scope)?.get(index)? + } + + /// Creates a new scope and returns it. + pub fn add_scope(&mut self) -> usize { + let scope_index = self.scopes.len(); + self.scopes.push(Vec::default()); + scope_index + } +} + +/// A contiguous list of entities identfied by their index in the list. +#[derive(Debug)] +pub struct ScopedEntities(Vec>); + +impl ScopedEntities { + /// Creates a new [`ScopedEntities`] with the given `size`, intialized to [`None`] (no [`Entity`] assigned). + pub fn new(size: usize) -> Self { + Self(vec![None; size]) + } +} + +impl ScopedEntities { + /// Gets the [`Entity`] assigned to the given (scope, index) pair, if it exists, and spawns a new entity if + /// it does not. + pub fn get( + &mut self, + world: &mut World, + entity_scopes: &EntityScopes, + scope: usize, + index: usize, + ) -> Entity { + let index = entity_scopes.get(scope, index).unwrap(); + *self.0[index].get_or_insert_with(|| world.spawn_empty().id()) + } + + /// Assigns the given `entity` to the (scope, index) pair. + pub fn set( + &mut self, + entity_scopes: &EntityScopes, + scope: usize, + index: usize, + entity: Entity, + ) { + let index = entity_scopes.get(scope, index).unwrap(); + self.0[index] = Some(entity); + } +} + +impl<'a> TemplateContext<'a> { + /// Retrieves a reference to the given resource `R`. + pub fn resource(&self) -> &R { + self.entity.resource() + } +} + +/// [`GetTemplate`] is implemented for types that can be produced by a specific, canonical [`Template`]. This creates a way to correlate to the [`Template`] using the +/// desired template output type. This is used by Bevy's scene system. +pub trait GetTemplate: Sized { + /// The [`Template`] for this type. + type Template: Template; +} + +macro_rules! template_impl { + ($($template: ident),*) => { + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such, the lints below may not always apply." + )] + impl<$($template: Template),*> Template for TemplateTuple<($($template,)*)> { + type Output = ($($template::Output,)*); + fn build(&mut self, _context: &mut TemplateContext) -> Result { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($template,)*) = &mut self.0; + Ok(($($template.build(_context)?,)*)) + } + + fn register_data(&self, _data: &mut TemplateData) { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($template,)*) = &self.0; + $($template.register_data(_data);)* + } + } + } +} + +/// A wrapper over a tuple of [`Template`] implementations, which also implements [`Template`]. This exists because [`Template`] cannot +/// be directly implemented for tuples of [`Template`] implementations. +pub struct TemplateTuple(pub T); + +all_tuples!(template_impl, 0, 12, T); + +impl Template for T { + type Output = T; + + fn build(&mut self, _context: &mut TemplateContext) -> Result { + Ok(self.clone()) + } +} + +impl GetTemplate for T { + type Template = T; +} + +/// A [`Template`] reference to an [`Entity`]. +pub enum EntityReference<'a> { + /// A reference to an entity via an [`EntityPath`] + Path(EntityPath<'a>), + /// An entity index within the current [`TemplateContext`], which is defined by a scope + /// and an index. This references a specific (and sometimes yet-to-be-spawned) entity defined + /// within a given scope. + /// + /// In most cases this is initialized by the scene system and should not be initialized manually. + /// Scopes must be defined ahead of time on the [`TemplateContext`]. + Index { + /// The scope of the entity index. This must be defined ahead of time. + scope: usize, + /// The index that uniquely identifies the entity within the current scope. + index: usize, + }, +} + +impl<'a> Default for EntityReference<'a> { + fn default() -> Self { + Self::Path(Default::default()) + } +} + +impl Template for EntityReference<'static> { + type Output = Entity; + + fn build(&mut self, context: &mut TemplateContext) -> Result { + Ok(match self { + EntityReference::Path(entity_path) => context.entity.resolve_path(entity_path)?, + // unwrap is ok as this is "internals". when implemented correctly this will never panic + EntityReference::Index { scope, index } => context.get_scoped_entity(*scope, *index), + }) + } +} + +impl GetTemplate for Entity { + type Template = EntityReference<'static>; +} + +/// A type-erased, object-safe, downcastable version of [`Template`]. +pub trait ErasedTemplate: Downcast + Send + Sync { + /// Applies this template to the given `entity`. + fn apply(&mut self, context: &mut TemplateContext) -> Result<(), BevyError>; +} + +impl_downcast!(ErasedTemplate); + +impl + Send + Sync + 'static> ErasedTemplate for T { + fn apply(&mut self, context: &mut TemplateContext) -> Result<(), BevyError> { + let bundle = self.build(context)?; + context.entity.insert(bundle); + Ok(()) + } +} + +// TODO: Consider cutting this +/// A [`Template`] implementation that holds _either_ a [`Template`] value _or_ the [`Template::Output`] value. +pub enum TemplateField { + /// A [`Template`]. + Template(T), + /// A [`Template::Output`]. + Value(T::Output), +} + +impl Default for TemplateField { + fn default() -> Self { + Self::Template(::default()) + } +} + +impl> Template for TemplateField { + type Output = T::Output; + + fn build(&mut self, context: &mut TemplateContext) -> Result { + Ok(match self { + TemplateField::Template(value) => value.build(context)?, + TemplateField::Value(value) => value.clone(), + }) + } +} + +/// This is used by the [`GetTemplate`] derive to work around [this Rust limitation](https://github.com/rust-lang/rust/issues/86935). +/// A fix is implemented and on track for stabilization. If it is ever implemented, we can remove this. +pub type Wrapper = T; + +/// A [`Template`] driven by a function that returns an output. This is used to create "free floating" templates without +/// defining a new type. See [`template`] for usage. +pub struct FnTemplate Result, O>(pub F); + +impl Result, O> Template for FnTemplate { + type Output = O; + + fn build(&mut self, context: &mut TemplateContext) -> Result { + (self.0)(context) + } +} + +/// Returns a "free floating" template for a given `func`. This prevents the need to define a custom type for one-off templates. +pub fn template Result, O>(func: F) -> FnTemplate { + FnTemplate(func) +} + +/// Arbitrary data storage which can be used by [`Template`] implementations to register metadata such as asset dependencies. +#[derive(Default)] +pub struct TemplateData(TypeIdMap>); + +impl TemplateData { + /// Adds the `value` to this storage. This will be added to the back of a list of other values of the same type. + pub fn add(&mut self, value: T) { + match self.0.entry(TypeId::of::()) { + Entry::Occupied(mut entry) => { + entry + .get_mut() + .downcast_mut::>() + .unwrap() + .push(value); + } + Entry::Vacant(entry) => { + entry.insert(Box::new(vec![value])); + } + } + } + + /// Iterates over all stored values of the given type `T`. + pub fn iter(&self) -> impl Iterator { + if let Some(value) = self.0.get(&TypeId::of::()) { + let value = value.downcast_ref::>().unwrap(); + value.iter() + } else { + [].iter() + } + } +} diff --git a/crates/bevy_feathers/Cargo.toml b/crates/bevy_feathers/Cargo.toml index b3e6af3c430cb..f6cd4fcfb30c5 100644 --- a/crates/bevy_feathers/Cargo.toml +++ b/crates/bevy_feathers/Cargo.toml @@ -25,6 +25,7 @@ bevy_shader = { path = "../bevy_shader", version = "0.17.1" } bevy_platform = { path = "../bevy_platform", version = "0.17.1" } bevy_reflect = { path = "../bevy_reflect", version = "0.17.1" } bevy_render = { path = "../bevy_render", version = "0.17.1" } +bevy_scene2 = { path = "../bevy_scene2", version = "0.17.1" } bevy_text = { path = "../bevy_text", version = "0.17.1" } bevy_ui = { path = "../bevy_ui", version = "0.17.1", features = [ "bevy_ui_picking_backend", diff --git a/crates/bevy_feathers/src/assets/icons/_NAMING.md b/crates/bevy_feathers/src/assets/icons/_NAMING.md new file mode 100644 index 0000000000000..baffdedb4103b --- /dev/null +++ b/crates/bevy_feathers/src/assets/icons/_NAMING.md @@ -0,0 +1,2 @@ +The names of files in this directory should refer to what the icons look like ("x", "chevron", etc.) +rather than their assigned meanings ("close", "expand") because the latter can change. diff --git a/crates/bevy_feathers/src/assets/icons/chevron-down.png b/crates/bevy_feathers/src/assets/icons/chevron-down.png new file mode 100644 index 0000000000000..ba8a4c42104e0 Binary files /dev/null and b/crates/bevy_feathers/src/assets/icons/chevron-down.png differ diff --git a/crates/bevy_feathers/src/assets/icons/chevron-right.png b/crates/bevy_feathers/src/assets/icons/chevron-right.png new file mode 100644 index 0000000000000..681c167ac0152 Binary files /dev/null and b/crates/bevy_feathers/src/assets/icons/chevron-right.png differ diff --git a/crates/bevy_feathers/src/assets/icons/x.png b/crates/bevy_feathers/src/assets/icons/x.png new file mode 100644 index 0000000000000..23cff4ac589c8 Binary files /dev/null and b/crates/bevy_feathers/src/assets/icons/x.png differ diff --git a/crates/bevy_feathers/src/constants.rs b/crates/bevy_feathers/src/constants.rs index 359e5a4935b0c..14789f0083c39 100644 --- a/crates/bevy_feathers/src/constants.rs +++ b/crates/bevy_feathers/src/constants.rs @@ -14,6 +14,16 @@ pub mod fonts { pub const MONO: &str = "embedded://bevy_feathers/assets/fonts/FiraMono-Medium.ttf"; } +/// Icon paths +pub mod icons { + /// Downward-pointing chevron + pub const CHEVRON_DOWN: &str = "embedded://bevy_feathers/assets/icons/chevron-down.png"; + /// Right-pointing chevron + pub const CHEVRON_RIGHT: &str = "embedded://bevy_feathers/assets/icons/chevron-right.png"; + /// Diagonal Cross + pub const X: &str = "embedded://bevy_feathers/assets/icons/x.png"; +} + /// Size constants pub mod size { use bevy_ui::Val; diff --git a/crates/bevy_feathers/src/controls/button.rs b/crates/bevy_feathers/src/controls/button.rs index 38695f679f373..67af699f312ab 100644 --- a/crates/bevy_feathers/src/controls/button.rs +++ b/crates/bevy_feathers/src/controls/button.rs @@ -1,19 +1,16 @@ use bevy_app::{Plugin, PreUpdate}; use bevy_ecs::{ - bundle::Bundle, component::Component, entity::Entity, - hierarchy::{ChildOf, Children}, lifecycle::RemovedComponents, query::{Added, Changed, Has, Or}, reflect::ReflectComponent, schedule::IntoScheduleConfigs, - spawn::{SpawnRelated, SpawnableList}, system::{Commands, Query}, }; -use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_scene2::{prelude::*, template_value}; use bevy_ui::{AlignItems, InteractionDisabled, JustifyContent, Node, Pressed, UiRect, Val}; use bevy_ui_widgets::Button; @@ -21,11 +18,11 @@ use crate::{ constants::{fonts, size}, cursor::EntityCursor, font_styles::InheritableFont, - handle_or_path::HandleOrPath, rounded_corners::RoundedCorners, theme::{ThemeBackgroundColor, ThemeFontColor}, tokens, }; +use bevy_input_focus::tab_navigation::TabIndex; /// Color variants for buttons. This also functions as a component used by the dynamic styling /// system to identify which entities are buttons. @@ -49,41 +46,61 @@ pub struct ButtonProps { pub corners: RoundedCorners, } -/// Template function to spawn a button. +/// Button scene function. /// /// # Arguments /// * `props` - construction properties for the button. -/// * `overrides` - a bundle of components that are merged in with the normal button components. -/// * `children` - a [`SpawnableList`] of child elements, such as a label or icon for the button. -pub fn button + Send + Sync + 'static, B: Bundle>( - props: ButtonProps, - overrides: B, - children: C, -) -> impl Bundle { - ( +pub fn button(props: ButtonProps) -> impl Scene { + bsn! { Node { height: size::ROW_HEIGHT, + min_width: size::ROW_HEIGHT, justify_content: JustifyContent::Center, align_items: AlignItems::Center, padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)), flex_grow: 1.0, - ..Default::default() - }, - Button, - props.variant, - Hovered::default(), - EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - props.corners.to_border_radius(4.0), - ThemeBackgroundColor(tokens::BUTTON_BG), - ThemeFontColor(tokens::BUTTON_TEXT), + } + Button + Hovered + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeBackgroundColor(tokens::BUTTON_BG) + ThemeFontColor(tokens::BUTTON_TEXT) + template_value(props.variant) + template_value(props.corners.to_border_radius(4.0)) InheritableFont { - font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font: fonts::REGULAR, font_size: 14.0, - }, - overrides, - Children::spawn(children), - ) + } + } +} + +/// Tool button scene function: a smaller button for embedding in panel headers. +/// +/// # Arguments +/// * `props` - construction properties for the button. +pub fn tool_button(props: ButtonProps) -> impl Scene { + bsn! { + Node { + height: size::ROW_HEIGHT, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)), + flex_grow: 1.0, + } + Button + Hovered + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeBackgroundColor(tokens::BUTTON_BG) + ThemeFontColor(tokens::BUTTON_TEXT) + InheritableFont { + font: fonts::REGULAR, + font_size: 14.0, + } + template_value(props.variant) + template_value(props.corners.to_border_radius(4.0)) + } } fn update_button_styles( diff --git a/crates/bevy_feathers/src/controls/checkbox.rs b/crates/bevy_feathers/src/controls/checkbox.rs index 01dec1edd645e..77948616e598b 100644 --- a/crates/bevy_feathers/src/controls/checkbox.rs +++ b/crates/bevy_feathers/src/controls/checkbox.rs @@ -1,22 +1,20 @@ use bevy_app::{Plugin, PreUpdate}; use bevy_camera::visibility::Visibility; use bevy_ecs::{ - bundle::Bundle, - children, component::Component, entity::Entity, - hierarchy::{ChildOf, Children}, + hierarchy::Children, lifecycle::RemovedComponents, query::{Added, Changed, Has, Or, With}, reflect::ReflectComponent, schedule::IntoScheduleConfigs, - spawn::{Spawn, SpawnRelated, SpawnableList}, system::{Commands, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_math::Rot2; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_scene2::prelude::*; use bevy_ui::{ AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, Node, PositionType, UiRect, UiTransform, Val, @@ -27,7 +25,6 @@ use crate::{ constants::{fonts, size}, cursor::EntityCursor, font_styles::InheritableFont, - handle_or_path::HandleOrPath, theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, tokens, }; @@ -47,71 +44,58 @@ struct CheckboxOutline; #[reflect(Component, Clone, Default)] struct CheckboxMark; -/// Template function to spawn a checkbox. +/// Checkbox scene function. /// /// # Arguments /// * `props` - construction properties for the checkbox. -/// * `overrides` - a bundle of components that are merged in with the normal checkbox components. -/// * `label` - the label of the checkbox. -pub fn checkbox + Send + Sync + 'static, B: Bundle>( - overrides: B, - label: C, -) -> impl Bundle { - ( +pub fn checkbox() -> impl Scene { + bsn! { Node { display: Display::Flex, flex_direction: FlexDirection::Row, justify_content: JustifyContent::Start, align_items: AlignItems::Center, column_gap: Val::Px(4.0), - ..Default::default() - }, - Checkbox, - CheckboxFrame, - Hovered::default(), - EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - ThemeFontColor(tokens::CHECKBOX_TEXT), + } + Checkbox + CheckboxFrame + Hovered + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeFontColor(tokens::CHECKBOX_TEXT) InheritableFont { - font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font: fonts::REGULAR, font_size: 14.0, - }, - overrides, - Children::spawn(( - Spawn(( + } + [( + Node { + width: size::CHECKBOX_SIZE, + height: size::CHECKBOX_SIZE, + border: UiRect::all(Val::Px(2.0)), + } + CheckboxOutline + BorderRadius::all(Val::Px(4.0)) + ThemeBackgroundColor(tokens::CHECKBOX_BG) + ThemeBorderColor(tokens::CHECKBOX_BORDER) + [( + // Cheesy checkmark: rotated node with L-shaped border. Node { - width: size::CHECKBOX_SIZE, - height: size::CHECKBOX_SIZE, - border: UiRect::all(Val::Px(2.0)), - ..Default::default() - }, - CheckboxOutline, - BorderRadius::all(Val::Px(4.0)), - ThemeBackgroundColor(tokens::CHECKBOX_BG), - ThemeBorderColor(tokens::CHECKBOX_BORDER), - children![( - // Cheesy checkmark: rotated node with L-shaped border. - Node { - position_type: PositionType::Absolute, - left: Val::Px(4.0), - top: Val::Px(0.0), - width: Val::Px(6.), - height: Val::Px(11.), - border: UiRect { - bottom: Val::Px(2.0), - right: Val::Px(2.0), - ..Default::default() - }, - ..Default::default() + position_type: PositionType::Absolute, + left: Val::Px(4.0), + top: Val::Px(0.0), + width: Val::Px(6.), + height: Val::Px(11.), + border: UiRect { + bottom: Val::Px(2.0), + right: Val::Px(2.0), }, - UiTransform::from_rotation(Rot2::FRAC_PI_4), - CheckboxMark, - ThemeBorderColor(tokens::CHECKBOX_MARK), - )], - )), - label, - )), - ) + } + UiTransform::from_rotation(Rot2::FRAC_PI_4) + CheckboxMark + ThemeBorderColor(tokens::CHECKBOX_MARK) + )] + )] + } } fn update_checkbox_styles( diff --git a/crates/bevy_feathers/src/controls/color_slider.rs b/crates/bevy_feathers/src/controls/color_slider.rs index b4dcdcbc7e02a..1dd1631d9906a 100644 --- a/crates/bevy_feathers/src/controls/color_slider.rs +++ b/crates/bevy_feathers/src/controls/color_slider.rs @@ -4,19 +4,17 @@ use bevy_app::{Plugin, PreUpdate}; use bevy_asset::Handle; use bevy_color::{Alpha, Color, Hsla}; use bevy_ecs::{ - bundle::Bundle, - children, component::Component, entity::Entity, hierarchy::Children, query::{Changed, Or, With}, schedule::IntoScheduleConfigs, - spawn::SpawnRelated, system::Query, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_log::warn_once; use bevy_picking::PickingSystems; +use bevy_scene2::{prelude::*, template_value}; use bevy_ui::{ AlignItems, BackgroundColor, BackgroundGradient, BorderColor, BorderRadius, ColorStop, Display, FlexDirection, Gradient, InterpolationColorSpace, LinearGradient, Node, Outline, PositionType, @@ -178,29 +176,27 @@ struct ColorSliderThumb; /// # Arguments /// /// * `props` - construction properties for the slider. -/// * `overrides` - a bundle of components that are merged in with the normal slider components. -pub fn color_slider(props: ColorSliderProps, overrides: B) -> impl Bundle { - ( +pub fn color_slider(props: ColorSliderProps) -> impl Scene { + let channel = props.channel.clone(); + bsn! { Node { display: Display::Flex, flex_direction: FlexDirection::Row, height: Val::Px(SLIDER_HEIGHT), align_items: AlignItems::Stretch, flex_grow: 1.0, - ..Default::default() - }, + } Slider { track_click: TrackClick::Snap, - }, + } ColorSlider { - channel: props.channel.clone(), - }, - SliderValue(props.value), - props.channel.range(), - EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - overrides, - children![ + channel: {channel.clone()}, + } + SliderValue({props.value}) + template_value(props.channel.range()) + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + [ // track ( Node { @@ -209,29 +205,26 @@ pub fn color_slider(props: ColorSliderProps, overrides: B) -> impl Bu right: Val::Px(0.), top: Val::Px(TRACK_PADDING), bottom: Val::Px(TRACK_PADDING), - ..Default::default() - }, - RoundedCorners::All.to_border_radius(TRACK_RADIUS), - ColorSliderTrack, - AlphaPattern, - MaterialNode::(Handle::default()), - children![ + } + template_value(RoundedCorners::All.to_border_radius(TRACK_RADIUS)) + ColorSliderTrack + AlphaPattern + MaterialNode::(Handle::default()) + [ // Left endcap ( Node { - width: Val::Px(THUMB_SIZE * 0.5), - ..Default::default() - }, - RoundedCorners::Left.to_border_radius(TRACK_RADIUS), - BackgroundColor(palette::X_AXIS), + width: Val::Px({THUMB_SIZE * 0.5}), + } + template_value(RoundedCorners::Left.to_border_radius(TRACK_RADIUS)) + BackgroundColor({palette::X_AXIS}) ), // Track with gradient ( Node { flex_grow: 1.0, - ..Default::default() - }, - BackgroundGradient(vec![Gradient::Linear(LinearGradient { + } + BackgroundGradient({vec![Gradient::Linear(LinearGradient { angle: PI * 0.5, stops: vec![ ColorStop::new(Color::NONE, Val::Percent(0.)), @@ -239,9 +232,9 @@ pub fn color_slider(props: ColorSliderProps, overrides: B) -> impl Bu ColorStop::new(Color::NONE, Val::Percent(100.)), ], color_space: InterpolationColorSpace::Srgba, - })]), - ZIndex(1), - children![( + })]}) + ZIndex(1) + [( Node { position_type: PositionType::Absolute, left: Val::Percent(0.), @@ -249,17 +242,16 @@ pub fn color_slider(props: ColorSliderProps, overrides: B) -> impl Bu width: Val::Px(THUMB_SIZE), height: Val::Px(THUMB_SIZE), border: UiRect::all(Val::Px(2.0)), - ..Default::default() - }, - SliderThumb, - ColorSliderThumb, - BorderRadius::MAX, - BorderColor::all(palette::WHITE), + } + SliderThumb + ColorSliderThumb + BorderRadius::MAX + BorderColor::all(palette::WHITE) Outline { width: Val::Px(1.), offset: Val::Px(0.), color: palette::BLACK - }, + } UiTransform::from_translation(Val2::new( Val::Percent(-50.0), Val::Percent(-50.0), @@ -269,16 +261,15 @@ pub fn color_slider(props: ColorSliderProps, overrides: B) -> impl Bu // Right endcap ( Node { - width: Val::Px(THUMB_SIZE * 0.5), - ..Default::default() - }, - RoundedCorners::Right.to_border_radius(TRACK_RADIUS), - BackgroundColor(palette::Z_AXIS), + width: Val::Px({THUMB_SIZE * 0.5}), + } + template_value(RoundedCorners::Right.to_border_radius(TRACK_RADIUS)) + BackgroundColor({palette::Z_AXIS}) ), ] - ), - ], - ) + ) + ] + } } fn update_slider_pos( @@ -292,10 +283,10 @@ fn update_slider_pos( q_children: Query<&Children>, mut q_slider_thumb: Query<&mut Node, With>, ) { - for (slider_ent, value, range) in q_sliders.iter_mut() { + for (slider_ent, SliderValue(value), range) in q_sliders.iter_mut() { for child in q_children.iter_descendants(slider_ent) { if let Ok(mut thumb_node) = q_slider_thumb.get_mut(child) { - thumb_node.left = Val::Percent(range.thumb_position(value.0) * 100.0); + thumb_node.left = Val::Percent(range.thumb_position(*value) * 100.0); } } } diff --git a/crates/bevy_feathers/src/controls/color_swatch.rs b/crates/bevy_feathers/src/controls/color_swatch.rs index 972f13b7878f5..e6e3231aa30da 100644 --- a/crates/bevy_feathers/src/controls/color_swatch.rs +++ b/crates/bevy_feathers/src/controls/color_swatch.rs @@ -1,11 +1,10 @@ use bevy_asset::Handle; use bevy_color::Alpha; -use bevy_ecs::{ - bundle::Bundle, children, component::Component, reflect::ReflectComponent, spawn::SpawnRelated, -}; +use bevy_ecs::{component::Component, reflect::ReflectComponent}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_scene2::{bsn, Scene}; use bevy_ui::{BackgroundColor, BorderRadius, Node, PositionType, Val}; -use bevy_ui_render::ui_material::MaterialNode; +use bevy_ui_render::prelude::MaterialNode; use crate::{ alpha_pattern::{AlphaPattern, AlphaPatternMaterial}, @@ -29,30 +28,27 @@ pub struct ColorSwatchFg; /// /// # Arguments /// * `overrides` - a bundle of components that are merged in with the normal swatch components. -pub fn color_swatch(overrides: B) -> impl Bundle { - ( +pub fn color_swatch() -> impl Scene { + bsn! { Node { height: size::ROW_HEIGHT, min_width: size::ROW_HEIGHT, - ..Default::default() - }, - ColorSwatch, - AlphaPattern, - MaterialNode::(Handle::default()), - BorderRadius::all(Val::Px(5.0)), - overrides, - children![( + } + ColorSwatch + AlphaPattern + MaterialNode::(Handle::default()) + BorderRadius::all(Val::Px(5.0)) + [ Node { position_type: PositionType::Absolute, left: Val::Px(0.), top: Val::Px(0.), bottom: Val::Px(0.), right: Val::Px(0.), - ..Default::default() - }, - ColorSwatchFg, - BackgroundColor(palette::ACCENT.with_alpha(0.5)), + } + ColorSwatchFg + BackgroundColor({palette::ACCENT.with_alpha(0.5)}) BorderRadius::all(Val::Px(5.0)) - ),], - ) + ] + } } diff --git a/crates/bevy_feathers/src/controls/menu.rs b/crates/bevy_feathers/src/controls/menu.rs new file mode 100644 index 0000000000000..cc30c2623835a --- /dev/null +++ b/crates/bevy_feathers/src/controls/menu.rs @@ -0,0 +1,329 @@ +use alloc::sync::Arc; + +use bevy_app::{Plugin, PreUpdate}; +use bevy_camera::visibility::Visibility; +use bevy_color::{Alpha, Srgba}; +use bevy_ecs::{ + component::Component, + entity::Entity, + hierarchy::Children, + lifecycle::RemovedComponents, + observer::On, + query::{Added, Changed, Has, Or, With}, + schedule::IntoScheduleConfigs, + system::{Commands, EntityCommands, Query}, +}; +use bevy_log::info; +use bevy_picking::{ + events::{Click, Pointer}, + hover::Hovered, + PickingSystems, +}; +use bevy_scene2::{prelude::*, template_value}; +use bevy_ui::{ + AlignItems, BoxShadow, Display, FlexDirection, GlobalZIndex, InteractionDisabled, + JustifyContent, Node, OverrideClip, PositionType, Pressed, UiRect, Val, +}; +use bevy_ui_widgets::{ + popover::{Popover, PopoverAlign, PopoverPlacement, PopoverSide}, + MenuAction, MenuEvent, MenuItem, MenuPopup, +}; + +use crate::{ + constants::{fonts, icons, size}, + controls::{button, ButtonProps, ButtonVariant}, + font_styles::InheritableFont, + icon, + rounded_corners::RoundedCorners, + theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, + tokens, +}; +use bevy_input_focus::tab_navigation::TabIndex; + +/// Parameters for the menu button template, passed to [`menu_button`] function. +#[derive(Default)] +pub struct MenuButtonProps { + /// Rounded corners options + pub corners: RoundedCorners, +} + +/// Marker for menu items +#[derive(Component, Default, Clone)] +struct MenuItemStyle; + +/// Marker for menu popup +#[derive(Component, Default, Clone)] +struct MenuPopupStyle; + +/// Marker for menu wrapper +#[derive(Component, Clone, Default)] +struct Menu(Option>); + +/// Menu scene function. This wraps the menu button and provides an anchor for the popopver. +pub fn menu(spawn_popover: F) -> impl Scene { + let menu = Menu(Some(Arc::new(spawn_popover))); + bsn! { + Node { + height: size::ROW_HEIGHT, + justify_content: JustifyContent::Stretch, + align_items: AlignItems::Stretch, + } + template_value(menu) + on(| + ev: On, + q_menu: Query<(&Menu, &Children)>, + q_popovers: Query>, + // mut redraw_events: MessageWriter, + mut commands: Commands| { + match ev.event().action { + // MenuEvent::Open => todo!(), + // MenuEvent::Close => todo!(), + MenuAction::Toggle => { + let mut was_open = false; + let Ok((menu, children)) = q_menu.get(ev.source) else { + return; + }; + for child in children.iter() { + if q_popovers.contains(*child) { + commands.entity(*child).despawn(); + was_open = true; + } + } + // Spawn the menu if not already open. + if !was_open { + info!("Opening, !was_open"); + if let Some(factory) = menu.0.as_ref() { + (*factory)(commands.entity(ev.source)); + // redraw_events.write(RequestRedraw); + } + } + }, + MenuAction::CloseAll => { + let Ok((_menu, children)) = q_menu.get(ev.source) else { + return; + }; + for child in children.iter() { + if q_popovers.contains(*child) { + commands.entity(*child).despawn(); + } + } + }, + // MenuEvent::FocusRoot => todo!(), + event => { + info!("Menu Event: {:?}", event); + } + } + }) + } +} + +/// Button scene function. +/// +/// # Arguments +/// * `props` - construction properties for the button. +pub fn menu_button(props: MenuButtonProps) -> impl Scene { + bsn! { + :button(ButtonProps { + variant: ButtonVariant::Normal, + corners: props.corners, + }) + Node { + // TODO: HACK to deal with lack of intercepted children + flex_direction: FlexDirection::RowReverse, + } + on(|ev: On>, mut commands: Commands| { + commands.trigger(MenuEvent { source: ev.entity, action: MenuAction::Toggle }); + }) + [ + :icon(icons::CHEVRON_DOWN), + Node { + flex_grow: 0.2, + } + ] + } +} + +/// Menu Popup scene function +pub fn menu_popup() -> impl Scene { + bsn! { + Node { + position_type: PositionType::Absolute, + display: Display::Flex, + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Stretch, + align_items: AlignItems::Stretch, + border: UiRect::all(Val::Px(1.0)), + padding: UiRect::all(Val::Px(4.0)), + } + MenuPopupStyle + MenuPopup + template_value(Visibility::Hidden) + template_value(RoundedCorners::All.to_border_radius(4.0)) + ThemeBackgroundColor(tokens::MENU_BG) + ThemeBorderColor(tokens::MENU_BORDER) + BoxShadow::new( + Srgba::BLACK.with_alpha(0.9).into(), + Val::Px(0.0), + Val::Px(0.0), + Val::Px(1.0), + Val::Px(4.0), + ) + GlobalZIndex(100) + template_value( + Popover { + positions: vec![ + PopoverPlacement { + side: PopoverSide::Bottom, + align: PopoverAlign::Start, + gap: 2.0, + }, + PopoverPlacement { + side: PopoverSide::Top, + align: PopoverAlign::Start, + gap: 2.0, + }, + ], + window_margin: 10.0, + } + ) + OverrideClip + } +} + +/// Menu item scene function +pub fn menu_item() -> impl Scene { + bsn! { + Node { + height: size::ROW_HEIGHT, + min_width: size::ROW_HEIGHT, + justify_content: JustifyContent::Start, + align_items: AlignItems::Center, + padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)), + } + MenuItemStyle + MenuItem + Hovered + // TODO: port CursonIcon to GetTemplate + // CursorIcon::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeBackgroundColor(tokens::MENU_BG) // Same as menu + ThemeFontColor(tokens::MENUITEM_TEXT) + InheritableFont { + font: fonts::REGULAR, + font_size: 14.0, + } + } +} + +fn update_menuitem_styles( + q_menuitems: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeBackgroundColor, + &ThemeFontColor, + ), + ( + With, + Or<(Changed, Added, Added)>, + ), + >, + mut commands: Commands, +) { + for (button_ent, disabled, pressed, hovered, bg_color, font_color) in q_menuitems.iter() { + set_menuitem_colors( + button_ent, + disabled, + pressed, + hovered.0, + bg_color, + font_color, + &mut commands, + ); + } +} + +fn update_menuitem_styles_remove( + q_menuitems: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeBackgroundColor, + &ThemeFontColor, + ), + With, + >, + mut removed_disabled: RemovedComponents, + mut removed_pressed: RemovedComponents, + mut commands: Commands, +) { + removed_disabled + .read() + .chain(removed_pressed.read()) + .for_each(|ent| { + if let Ok((button_ent, disabled, pressed, hovered, bg_color, font_color)) = + q_menuitems.get(ent) + { + set_menuitem_colors( + button_ent, + disabled, + pressed, + hovered.0, + bg_color, + font_color, + &mut commands, + ); + } + }); +} + +fn set_menuitem_colors( + button_ent: Entity, + disabled: bool, + pressed: bool, + hovered: bool, + bg_color: &ThemeBackgroundColor, + font_color: &ThemeFontColor, + commands: &mut Commands, +) { + let bg_token = match (pressed, hovered) { + (true, _) => tokens::MENUITEM_BG_PRESSED, + (false, true) => tokens::MENUITEM_BG_HOVER, + (false, false) => tokens::MENU_BG, + }; + + let font_color_token = match disabled { + true => tokens::MENUITEM_TEXT_DISABLED, + false => tokens::MENUITEM_TEXT, + }; + + // Change background color + if bg_color.0 != bg_token { + commands + .entity(button_ent) + .insert(ThemeBackgroundColor(bg_token)); + } + + // Change font color + if font_color.0 != font_color_token { + commands + .entity(button_ent) + .insert(ThemeFontColor(font_color_token)); + } +} + +/// Plugin which registers the systems for updating the menu and menu button styles. +pub struct MenuPlugin; + +impl Plugin for MenuPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + (update_menuitem_styles, update_menuitem_styles_remove).in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/controls/mod.rs b/crates/bevy_feathers/src/controls/mod.rs index 8adf2b5911659..aacfb30923eef 100644 --- a/crates/bevy_feathers/src/controls/mod.rs +++ b/crates/bevy_feathers/src/controls/mod.rs @@ -5,21 +5,21 @@ mod button; mod checkbox; mod color_slider; mod color_swatch; +mod menu; mod radio; mod slider; mod toggle_switch; mod virtual_keyboard; -pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant}; -pub use checkbox::{checkbox, CheckboxPlugin}; -pub use color_slider::{ - color_slider, ColorChannel, ColorSlider, ColorSliderPlugin, ColorSliderProps, SliderBaseColor, -}; -pub use color_swatch::{color_swatch, ColorSwatch, ColorSwatchFg}; -pub use radio::{radio, RadioPlugin}; -pub use slider::{slider, SliderPlugin, SliderProps}; -pub use toggle_switch::{toggle_switch, ToggleSwitchPlugin}; -pub use virtual_keyboard::{virtual_keyboard, VirtualKeyPressed}; +pub use button::*; +pub use checkbox::*; +pub use color_slider::*; +pub use color_swatch::*; +pub use menu::*; +pub use radio::*; +pub use slider::*; +pub use toggle_switch::*; +pub use virtual_keyboard::*; use crate::alpha_pattern::AlphaPatternPlugin; @@ -33,6 +33,7 @@ impl Plugin for ControlsPlugin { ButtonPlugin, CheckboxPlugin, ColorSliderPlugin, + MenuPlugin, RadioPlugin, SliderPlugin, ToggleSwitchPlugin, diff --git a/crates/bevy_feathers/src/controls/radio.rs b/crates/bevy_feathers/src/controls/radio.rs index 0d90374955eb9..ab8c4dda9bfca 100644 --- a/crates/bevy_feathers/src/controls/radio.rs +++ b/crates/bevy_feathers/src/controls/radio.rs @@ -1,16 +1,13 @@ use bevy_app::{Plugin, PreUpdate}; use bevy_camera::visibility::Visibility; use bevy_ecs::{ - bundle::Bundle, - children, component::Component, entity::Entity, - hierarchy::{ChildOf, Children}, + hierarchy::Children, lifecycle::RemovedComponents, query::{Added, Changed, Has, Or, With}, reflect::ReflectComponent, schedule::IntoScheduleConfigs, - spawn::{Spawn, SpawnRelated, SpawnableList}, system::{Commands, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; @@ -26,10 +23,10 @@ use crate::{ constants::{fonts, size}, cursor::EntityCursor, font_styles::InheritableFont, - handle_or_path::HandleOrPath, theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, tokens, }; +use bevy_scene2::prelude::*; /// Marker for the radio outline #[derive(Component, Default, Clone, Reflect)] @@ -41,64 +38,49 @@ struct RadioOutline; #[reflect(Component, Clone, Default)] struct RadioMark; -/// Template function to spawn a radio. -/// -/// # Arguments -/// * `props` - construction properties for the radio. -/// * `overrides` - a bundle of components that are merged in with the normal radio components. -/// * `label` - the label of the radio. -pub fn radio + Send + Sync + 'static, B: Bundle>( - overrides: B, - label: C, -) -> impl Bundle { - ( +/// Radio scene function. +pub fn radio() -> impl Scene { + bsn! { Node { display: Display::Flex, flex_direction: FlexDirection::Row, justify_content: JustifyContent::Start, align_items: AlignItems::Center, column_gap: Val::Px(4.0), - ..Default::default() - }, - RadioButton, - Hovered::default(), - EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - ThemeFontColor(tokens::RADIO_TEXT), + } + RadioButton + Hovered + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeFontColor(tokens::RADIO_TEXT) InheritableFont { - font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font: fonts::REGULAR, font_size: 14.0, - }, - overrides, - Children::spawn(( - Spawn(( + } + [( + Node { + display: Display::Flex, + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + width: size::RADIO_SIZE, + height: size::RADIO_SIZE, + border: UiRect::all(Val::Px(2.0)), + } + RadioOutline + BorderRadius::MAX + ThemeBorderColor(tokens::RADIO_BORDER) + [( + // Cheesy checkmark: rotated node with L-shaped border. Node { - display: Display::Flex, - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - width: size::RADIO_SIZE, - height: size::RADIO_SIZE, - border: UiRect::all(Val::Px(2.0)), - ..Default::default() - }, - RadioOutline, - BorderRadius::MAX, - ThemeBorderColor(tokens::RADIO_BORDER), - children![( - // Cheesy checkmark: rotated node with L-shaped border. - Node { - width: Val::Px(8.), - height: Val::Px(8.), - ..Default::default() - }, - BorderRadius::MAX, - RadioMark, - ThemeBackgroundColor(tokens::RADIO_MARK), - )], - )), - label, - )), - ) + width: Val::Px(8.), + height: Val::Px(8.), + } + BorderRadius::MAX + RadioMark + ThemeBackgroundColor(tokens::RADIO_MARK) + )] + )] + } } fn update_radio_styles( diff --git a/crates/bevy_feathers/src/controls/slider.rs b/crates/bevy_feathers/src/controls/slider.rs index 1bcb2449f9370..596cb6673132e 100644 --- a/crates/bevy_feathers/src/controls/slider.rs +++ b/crates/bevy_feathers/src/controls/slider.rs @@ -3,8 +3,6 @@ use core::f32::consts::PI; use bevy_app::{Plugin, PreUpdate}; use bevy_color::Color; use bevy_ecs::{ - bundle::Bundle, - children, component::Component, entity::Entity, hierarchy::Children, @@ -12,12 +10,12 @@ use bevy_ecs::{ query::{Added, Changed, Has, Or, Spawned, With}, reflect::ReflectComponent, schedule::IntoScheduleConfigs, - spawn::SpawnRelated, system::{Commands, Query, Res}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::PickingSystems; use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_scene2::{prelude::*, template_value}; use bevy_ui::{ widget::Text, AlignItems, BackgroundGradient, ColorStop, Display, FlexDirection, Gradient, InteractionDisabled, InterpolationColorSpace, JustifyContent, LinearGradient, Node, @@ -29,7 +27,6 @@ use crate::{ constants::{fonts, size}, cursor::EntityCursor, font_styles::InheritableFont, - handle_or_path::HandleOrPath, rounded_corners::RoundedCorners, theme::{ThemeFontColor, ThemedText, UiTheme}, tokens, @@ -55,9 +52,8 @@ impl Default for SliderProps { } } -#[derive(Component, Default, Clone)] +#[derive(Component, Default, Clone, Reflect)] #[require(Slider)] -#[derive(Reflect)] #[reflect(Component, Clone, Default)] struct SliderStyle; @@ -66,33 +62,31 @@ struct SliderStyle; #[reflect(Component, Clone, Default)] struct SliderValueText; -/// Spawn a new slider widget. +/// Slider scene function. /// /// # Arguments /// /// * `props` - construction properties for the slider. -/// * `overrides` - a bundle of components that are merged in with the normal slider components. -pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { - ( +pub fn slider(props: SliderProps) -> impl Scene { + bsn! { Node { height: size::ROW_HEIGHT, justify_content: JustifyContent::Center, align_items: AlignItems::Center, padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)), flex_grow: 1.0, - ..Default::default() - }, + } Slider { track_click: TrackClick::Drag, - }, - SliderStyle, - SliderValue(props.value), - SliderRange::new(props.min, props.max), - EntityCursor::System(bevy_window::SystemCursorIcon::EwResize), - TabIndex(0), - RoundedCorners::All.to_border_radius(6.0), + } + SliderStyle + SliderValue({props.value}) + SliderRange::new(props.min, props.max) + EntityCursor::System(bevy_window::SystemCursorIcon::EwResize) + TabIndex(0) + template_value(RoundedCorners::All.to_border_radius(6.0)) // Use a gradient to draw the moving bar - BackgroundGradient(vec![Gradient::Linear(LinearGradient { + BackgroundGradient({vec![Gradient::Linear(LinearGradient { angle: PI * 0.5, stops: vec![ ColorStop::new(Color::NONE, Val::Percent(0.)), @@ -101,9 +95,8 @@ pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { ColorStop::new(Color::NONE, Val::Percent(100.)), ], color_space: InterpolationColorSpace::Srgba, - })]), - overrides, - children![( + })]}) + [( // Text container Node { display: Display::Flex, @@ -111,16 +104,15 @@ pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { flex_direction: FlexDirection::Row, align_items: AlignItems::Center, justify_content: JustifyContent::Center, - ..Default::default() - }, - ThemeFontColor(tokens::SLIDER_TEXT), + } + ThemeFontColor(tokens::SLIDER_TEXT) InheritableFont { - font: HandleOrPath::Path(fonts::MONO.to_owned()), + font: fonts::MONO, font_size: 12.0, - }, - children![(Text::new("10.0"), ThemedText, SliderValueText,)], - )], - ) + } + [(Text::new("10.0") ThemedText SliderValueText)] + )] + } } fn update_slider_styles( diff --git a/crates/bevy_feathers/src/controls/toggle_switch.rs b/crates/bevy_feathers/src/controls/toggle_switch.rs index dea53f169f0f8..db6e0e4ad58c0 100644 --- a/crates/bevy_feathers/src/controls/toggle_switch.rs +++ b/crates/bevy_feathers/src/controls/toggle_switch.rs @@ -2,8 +2,6 @@ use accesskit::Role; use bevy_a11y::AccessibilityNode; use bevy_app::{Plugin, PreUpdate}; use bevy_ecs::{ - bundle::Bundle, - children, component::Component, entity::Entity, hierarchy::Children, @@ -11,13 +9,13 @@ use bevy_ecs::{ query::{Added, Changed, Has, Or, With}, reflect::ReflectComponent, schedule::IntoScheduleConfigs, - spawn::SpawnRelated, system::{Commands, Query}, world::Mut, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_scene2::prelude::*; use bevy_ui::{BorderRadius, Checked, InteractionDisabled, Node, PositionType, UiRect, Val}; use bevy_ui_widgets::Checkbox; @@ -38,43 +36,39 @@ struct ToggleSwitchOutline; #[reflect(Component, Clone, Default)] struct ToggleSwitchSlide; -/// Template function to spawn a toggle switch. +/// Toggle switch scene function. /// /// # Arguments /// * `props` - construction properties for the toggle switch. -/// * `overrides` - a bundle of components that are merged in with the normal toggle switch components. -pub fn toggle_switch(overrides: B) -> impl Bundle { - ( +pub fn toggle_switch() -> impl Scene { + bsn! { Node { width: size::TOGGLE_WIDTH, height: size::TOGGLE_HEIGHT, border: UiRect::all(Val::Px(2.0)), - ..Default::default() - }, - Checkbox, - ToggleSwitchOutline, - BorderRadius::all(Val::Px(5.0)), - ThemeBackgroundColor(tokens::SWITCH_BG), - ThemeBorderColor(tokens::SWITCH_BORDER), - AccessibilityNode(accesskit::Node::new(Role::Switch)), - Hovered::default(), - EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - overrides, - children![( + } + Checkbox + ToggleSwitchOutline + BorderRadius::all(Val::Px(5.0)) + ThemeBackgroundColor(tokens::SWITCH_BG) + ThemeBorderColor(tokens::SWITCH_BORDER) + AccessibilityNode(accesskit::Node::new(Role::Switch)) + Hovered + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + [ Node { position_type: PositionType::Absolute, left: Val::Percent(0.), top: Val::Px(0.), bottom: Val::Px(0.), width: Val::Percent(50.), - ..Default::default() - }, - BorderRadius::all(Val::Px(3.0)), - ToggleSwitchSlide, - ThemeBackgroundColor(tokens::SWITCH_SLIDE), - )], - ) + } + BorderRadius::all(Val::Px(3.0)) + ToggleSwitchSlide + ThemeBackgroundColor(tokens::SWITCH_SLIDE) + ] + } } fn update_switch_styles( diff --git a/crates/bevy_feathers/src/controls/virtual_keyboard.rs b/crates/bevy_feathers/src/controls/virtual_keyboard.rs index 0e95a41956137..a7e599cab8841 100644 --- a/crates/bevy_feathers/src/controls/virtual_keyboard.rs +++ b/crates/bevy_feathers/src/controls/virtual_keyboard.rs @@ -1,9 +1,8 @@ use bevy_ecs::prelude::*; use bevy_input_focus::tab_navigation::TabGroup; -use bevy_ui::Node; -use bevy_ui::Val; -use bevy_ui::{widget::Text, FlexDirection}; -use bevy_ui_widgets::{observe, Activate}; +use bevy_scene2::prelude::*; +use bevy_ui::{widget::Text, FlexDirection, Node, Val}; +use bevy_ui_widgets::Activate; use crate::controls::{button, ButtonProps}; @@ -17,46 +16,52 @@ pub struct VirtualKeyPressed { } /// Function to spawn a virtual keyboard -pub fn virtual_keyboard( - keys: impl Iterator> + Send + Sync + 'static, -) -> impl Bundle +pub fn virtual_keyboard(keys: impl Iterator> + Send + Sync + 'static) -> impl Scene where T: AsRef + Clone + Send + Sync + 'static, { - ( + let keys = Vec::from_iter(keys.map(move |row| { + let key_row = Vec::from_iter(row.into_iter().map(move |key| { + let key_clone = key.clone(); + bsn! { + button(ButtonProps::default()) + on( + move |activate: On, + mut commands: Commands, + query: Query<&ChildOf>| + -> Result { + let virtual_keyboard = + query.get(query.get(activate.entity)?.parent())?.parent(); + commands.trigger(VirtualKeyPressed { + entity: virtual_keyboard, + key: key.clone(), + }); + Ok(()) + }, + ) + [ + Text::new(key_clone.as_ref()) + ] + } + })); + bsn! { + Node { + flex_direction: FlexDirection::Row, + column_gap: Val::Px(4.), + } + [ + {key_row} + ] + } + })); + bsn! { Node { flex_direction: FlexDirection::Column, row_gap: Val::Px(4.), - ..Default::default() - }, - TabGroup::new(0), - Children::spawn(SpawnIter(keys.map(move |row| { - ( - Node { - flex_direction: FlexDirection::Row, - column_gap: Val::Px(4.), - ..Default::default() - }, - Children::spawn(SpawnIter(row.into_iter().map(move |key| { - ( - button(ButtonProps::default(), (), Spawn(Text::new(key.as_ref()))), - observe( - move |activate: On, - mut commands: Commands, - query: Query<&ChildOf>| - -> Result { - let virtual_keyboard = - query.get(query.get(activate.entity)?.parent())?.parent(); - commands.trigger(VirtualKeyPressed { - entity: virtual_keyboard, - key: key.clone(), - }); - Ok(()) - }, - ), - ) - }))), - ) - }))), - ) + } + TabGroup::new(0) + [ + {keys} + ] + } } diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs index 4a35e40a7af20..0c69a0db1b087 100644 --- a/crates/bevy_feathers/src/cursor.rs +++ b/crates/bevy_feathers/src/cursor.rs @@ -9,6 +9,7 @@ use bevy_ecs::{ resource::Resource, schedule::IntoScheduleConfigs, system::{Commands, Query, Res}, + VariantDefaults, }; use bevy_picking::{hover::HoverMap, pointer::PointerId, PickingSystems}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -27,7 +28,7 @@ pub struct DefaultCursor(pub EntityCursor); /// /// This is effectively the same type as [`CustomCursor`] but with different methods, and used /// in different places. -#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)] +#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq, VariantDefaults)] #[reflect(Component, Debug, Default, PartialEq, Clone)] pub enum EntityCursor { #[cfg(feature = "custom_cursor")] diff --git a/crates/bevy_feathers/src/dark_theme.rs b/crates/bevy_feathers/src/dark_theme.rs index 4a7248852c51a..31a6ab3719ca6 100644 --- a/crates/bevy_feathers/src/dark_theme.rs +++ b/crates/bevy_feathers/src/dark_theme.rs @@ -95,6 +95,16 @@ pub fn create_dark_theme() -> ThemeProps { tokens::SWITCH_SLIDE_DISABLED, palette::LIGHT_GRAY_2.with_alpha(0.3), ), + // Menus + (tokens::MENU_BG, palette::GRAY_1), + (tokens::MENU_BORDER, palette::WARM_GRAY_1), + (tokens::MENUITEM_BG_HOVER, palette::GRAY_1.lighter(0.05)), + (tokens::MENUITEM_BG_PRESSED, palette::GRAY_1.lighter(0.1)), + (tokens::MENUITEM_TEXT, palette::WHITE), + ( + tokens::MENUITEM_TEXT_DISABLED, + palette::WHITE.with_alpha(0.5), + ), ]), } } diff --git a/crates/bevy_feathers/src/font_styles.rs b/crates/bevy_feathers/src/font_styles.rs index 9ea7783db5752..a77bd570ccff5 100644 --- a/crates/bevy_feathers/src/font_styles.rs +++ b/crates/bevy_feathers/src/font_styles.rs @@ -1,65 +1,39 @@ //! A framework for inheritable font styles. +use crate::theme::ThemedText; use bevy_app::{Propagate, PropagateOver}; -use bevy_asset::{AssetServer, Handle}; +use bevy_asset::Handle; use bevy_ecs::{ component::Component, lifecycle::Insert, observer::On, - reflect::ReflectComponent, - system::{Commands, Query, Res}, + system::{Commands, Query}, + template::GetTemplate, }; -use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_reflect::Reflect; use bevy_text::{Font, TextFont}; -use crate::{handle_or_path::HandleOrPath, theme::ThemedText}; - /// A component which, when inserted on an entity, will load the given font and propagate it -/// downward to any child text entity that has the [`ThemedText`] marker. -#[derive(Component, Default, Clone, Debug, Reflect)] -#[reflect(Component, Default)] +/// downward to any child text entity that has the [`ThemedText`](crate::theme::ThemedText) marker. +#[derive(Component, Clone, Debug, Reflect, GetTemplate)] #[require(ThemedText, PropagateOver::::default())] pub struct InheritableFont { /// The font handle or path. - pub font: HandleOrPath, + pub font: Handle, /// The desired font size. pub font_size: f32, } -impl InheritableFont { - /// Create a new `InheritableFont` from a handle. - pub fn from_handle(handle: Handle) -> Self { - Self { - font: HandleOrPath::Handle(handle), - font_size: 16.0, - } - } - - /// Create a new `InheritableFont` from a path. - pub fn from_path(path: &str) -> Self { - Self { - font: HandleOrPath::Path(path.to_string()), - font_size: 16.0, - } - } -} - -/// An observer which looks for changes to the [`InheritableFont`] component on an entity, and +/// An observer which looks for changes to the `InheritableFont` component on an entity, and /// propagates downward the font to all participating text entities. pub(crate) fn on_changed_font( insert: On, font_style: Query<&InheritableFont>, - assets: Res, mut commands: Commands, ) { - if let Ok(style) = font_style.get(insert.entity) - && let Some(font) = match style.font { - HandleOrPath::Handle(ref h) => Some(h.clone()), - HandleOrPath::Path(ref p) => Some(assets.load::(p)), - } - { + if let Ok(inheritable_font) = font_style.get(insert.entity) { commands.entity(insert.entity).insert(Propagate(TextFont { - font, - font_size: style.font_size, + font: inheritable_font.font.clone(), + font_size: inheritable_font.font_size, ..Default::default() })); } diff --git a/crates/bevy_feathers/src/icon.rs b/crates/bevy_feathers/src/icon.rs new file mode 100644 index 0000000000000..29a93520045ae --- /dev/null +++ b/crates/bevy_feathers/src/icon.rs @@ -0,0 +1,18 @@ +//! BSN Template for loading images and displaying them as [`ImageNodes`]. +use bevy_asset::AssetServer; +use bevy_ecs::template::template; +use bevy_scene2::{bsn, Scene}; +use bevy_ui::{widget::ImageNode, Node, Val}; + +/// Template which displays an icon. +pub fn icon(image: &'static str) -> impl Scene { + bsn! { + Node { + height: Val::Px(14.0), + } + template(move |entity| { + let handle = entity.resource::().load(image); + Ok(ImageNode::new(handle)) + }) + } +} diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs index 348b677f98e53..7752662f5fb18 100644 --- a/crates/bevy_feathers/src/lib.rs +++ b/crates/bevy_feathers/src/lib.rs @@ -18,6 +18,8 @@ //! Please report issues, submit fixes and propose changes. //! Thanks for stress-testing; let's build something better together. +extern crate alloc; + use bevy_app::{ HierarchyPropagatePlugin, Plugin, PluginGroup, PluginGroupBuilder, PostUpdate, PropagateSet, }; @@ -43,11 +45,14 @@ pub mod cursor; pub mod dark_theme; pub mod font_styles; pub mod handle_or_path; +mod icon; pub mod palette; pub mod rounded_corners; pub mod theme; pub mod tokens; +pub use icon::icon; + /// Plugin which installs observers and systems for feathers themes, cursors, and all controls. pub struct FeathersPlugin; @@ -62,6 +67,11 @@ impl Plugin for FeathersPlugin { embedded_asset!(app, "assets/fonts/FiraSans-Italic.ttf"); embedded_asset!(app, "assets/fonts/FiraMono-Medium.ttf"); + // Embedded icons + embedded_asset!(app, "assets/icons/chevron-down.png"); + embedded_asset!(app, "assets/icons/chevron-right.png"); + embedded_asset!(app, "assets/icons/x.png"); + // Embedded shader embedded_asset!(app, "assets/shaders/alpha_pattern.wgsl"); diff --git a/crates/bevy_feathers/src/theme.rs b/crates/bevy_feathers/src/theme.rs index 07c7cb1cbf48c..913cd8ddef078 100644 --- a/crates/bevy_feathers/src/theme.rs +++ b/crates/bevy_feathers/src/theme.rs @@ -19,7 +19,7 @@ use bevy_ui::{BackgroundColor, BorderColor}; use smol_str::SmolStr; /// A design token for the theme. This serves as the lookup key for the theme properties. -#[derive(Clone, PartialEq, Eq, Hash, Reflect)] +#[derive(Clone, PartialEq, Eq, Hash, Reflect, Default)] pub struct ThemeToken(SmolStr); impl ThemeToken { @@ -84,33 +84,30 @@ impl UiTheme { } /// Component which causes the background color of an entity to be set based on a theme color. -#[derive(Component, Clone)] +#[derive(Component, Clone, Default, Reflect)] #[require(BackgroundColor)] #[component(immutable)] -#[derive(Reflect)] #[reflect(Component, Clone)] pub struct ThemeBackgroundColor(pub ThemeToken); /// Component which causes the border color of an entity to be set based on a theme color. /// Only supports setting all borders to the same color. -#[derive(Component, Clone)] +#[derive(Component, Clone, Default, Reflect)] #[require(BorderColor)] #[component(immutable)] -#[derive(Reflect)] #[reflect(Component, Clone)] pub struct ThemeBorderColor(pub ThemeToken); /// Component which causes the inherited text color of an entity to be set based on a theme color. -#[derive(Component, Clone)] +#[derive(Component, Clone, Default, Reflect)] #[component(immutable)] -#[derive(Reflect)] #[reflect(Component, Clone)] #[require(ThemedText, PropagateOver::::default())] pub struct ThemeFontColor(pub ThemeToken); /// A marker component that is used to indicate that the text entity wants to opt-in to using /// inherited text styles. -#[derive(Component, Reflect, Default)] +#[derive(Component, Reflect, Default, Clone)] #[reflect(Component)] pub struct ThemedText; diff --git a/crates/bevy_feathers/src/tokens.rs b/crates/bevy_feathers/src/tokens.rs index a00a78bc799c0..9380b809799ee 100644 --- a/crates/bevy_feathers/src/tokens.rs +++ b/crates/bevy_feathers/src/tokens.rs @@ -137,3 +137,19 @@ pub const SWITCH_SLIDE: ThemeToken = ThemeToken::new_static("feathers.switch.sli /// Switch slide (disabled) pub const SWITCH_SLIDE_DISABLED: ThemeToken = ThemeToken::new_static("feathers.switch.slide.disabled"); + +// Menus + +/// Menu background +pub const MENU_BG: ThemeToken = ThemeToken::new_static("feathers.menu.bg"); +/// Menu border +pub const MENU_BORDER: ThemeToken = ThemeToken::new_static("feathers.menu.border"); +/// Menu item hovered +pub const MENUITEM_BG_HOVER: ThemeToken = ThemeToken::new_static("feathers.menuitem.bg.hover"); +/// Menu item pressed +pub const MENUITEM_BG_PRESSED: ThemeToken = ThemeToken::new_static("feathers.menuitem.bg.pressed"); +/// Menu item text +pub const MENUITEM_TEXT: ThemeToken = ThemeToken::new_static("feathers.menuitem.text"); +/// Menu item text (disabled) +pub const MENUITEM_TEXT_DISABLED: ThemeToken = + ThemeToken::new_static("feathers.menuitem.text.disabled"); diff --git a/crates/bevy_gizmos/src/retained.rs b/crates/bevy_gizmos/src/retained.rs index 16b60db7d3a61..40daad0cbad85 100644 --- a/crates/bevy_gizmos/src/retained.rs +++ b/crates/bevy_gizmos/src/retained.rs @@ -6,7 +6,7 @@ use bevy_asset::Handle; #[cfg(feature = "bevy_render")] use bevy_camera::visibility::RenderLayers; use bevy_ecs::{component::Component, reflect::ReflectComponent}; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_reflect::Reflect; use bevy_transform::components::Transform; #[cfg(feature = "bevy_render")] @@ -74,8 +74,8 @@ impl DerefMut for GizmoAsset { /// ``` /// /// [`Gizmos`]: crate::gizmos::Gizmos -#[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component, Clone, Default)] +#[derive(Component, Clone, Debug, Reflect)] +#[reflect(Component, Clone)] #[require(Transform)] pub struct Gizmo { /// The handle to the gizmo to draw. @@ -97,6 +97,16 @@ pub struct Gizmo { pub depth_bias: f32, } +impl Default for Gizmo { + fn default() -> Self { + Self { + handle: Handle::default(), + line_config: Default::default(), + depth_bias: Default::default(), + } + } +} + #[cfg(feature = "bevy_render")] pub(crate) fn extract_linegizmos( mut commands: Commands, diff --git a/crates/bevy_image/src/texture_atlas.rs b/crates/bevy_image/src/texture_atlas.rs index b7a7a715a360c..fcd08bd6e372a 100644 --- a/crates/bevy_image/src/texture_atlas.rs +++ b/crates/bevy_image/src/texture_atlas.rs @@ -203,7 +203,7 @@ impl TextureAtlasLayout { /// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs) /// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs) /// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs) -#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -216,6 +216,15 @@ pub struct TextureAtlas { pub index: usize, } +impl Default for TextureAtlas { + fn default() -> Self { + Self { + layout: Handle::default(), + index: Default::default(), + } + } +} + impl TextureAtlas { /// Retrieves the current texture [`URect`] of the sprite sheet according to the section `index` pub fn texture_rect(&self, texture_atlases: &Assets) -> Option { diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index a490d00dda10e..e112c84d5ea26 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -167,12 +167,12 @@ pub struct TabNavigation<'w, 's> { } impl TabNavigation<'_, '_> { - /// Navigate to the desired focusable entity. + /// Navigate to the desired focusable entity, relative to the current focused entity. /// /// Change the [`NavAction`] to navigate in a different direction. /// Focusable entities are determined by the presence of the [`TabIndex`] component. /// - /// If no focusable entities are found, then this function will return either the first + /// If there is no currently focused entity, then this function will return either the first /// or last focusable entity, depending on the direction of navigation. For example, if /// `action` is `Next` and no focusable entities are found, then this function will return /// the first focusable entity. @@ -199,13 +199,46 @@ impl TabNavigation<'_, '_> { }) }); + self.navigate_internal(focus.0, action, tabgroup) + } + + /// Initialize focus to a focusable child of a container, either the first or last + /// depending on [`NavAction`]. This assumes that the parent entity has a [`TabGroup`] + /// component. + /// + /// Focusable entities are determined by the presence of the [`TabIndex`] component. + pub fn initialize( + &self, + parent: Entity, + action: NavAction, + ) -> Result { + // If there are no tab groups, then there are no focusable entities. + if self.tabgroup_query.is_empty() { + return Err(TabNavigationError::NoTabGroups); + } + + // Look for the tab group on the parent entity. + match self.tabgroup_query.get(parent) { + Ok(tabgroup) => self.navigate_internal(None, action, Some((parent, tabgroup.1))), + Err(_) => Err(TabNavigationError::NoTabGroups), + } + } + + pub fn navigate_internal( + &self, + focus: Option, + action: NavAction, + tabgroup: Option<(Entity, &TabGroup)>, + ) -> Result { let navigation_result = self.navigate_in_group(tabgroup, focus, action); match navigation_result { Ok(entity) => { - if focus.0.is_some() && tabgroup.is_none() { + if let Some(previous_focus) = focus + && tabgroup.is_none() + { Err(TabNavigationError::NoTabGroupForCurrentFocus { - previous_focus: focus.0.unwrap(), + previous_focus, new_focus: entity, }) } else { @@ -219,7 +252,7 @@ impl TabNavigation<'_, '_> { fn navigate_in_group( &self, tabgroup: Option<(Entity, &TabGroup)>, - focus: &InputFocus, + focus: Option, action: NavAction, ) -> Result { // List of all focusable entities found. @@ -269,7 +302,7 @@ impl TabNavigation<'_, '_> { } }); - let index = focusable.iter().position(|e| Some(e.0) == focus.0); + let index = focusable.iter().position(|e| Some(e.0) == focus); let count = focusable.len(); let next = match (index, action) { (Some(idx), NavAction::Next) => (idx + 1).rem_euclid(count), diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 2b18540acafab..31aa8f68fd78d 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -496,6 +496,7 @@ bevy_picking = { path = "../bevy_picking", optional = true, version = "0.17.1" } bevy_remote = { path = "../bevy_remote", optional = true, version = "0.17.1" } bevy_render = { path = "../bevy_render", optional = true, version = "0.17.1" } bevy_scene = { path = "../bevy_scene", optional = true, version = "0.17.1" } +bevy_scene2 = { path = "../bevy_scene2", optional = true, version = "0.17.1" } bevy_solari = { path = "../bevy_solari", optional = true, version = "0.17.1" } bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.17.1" } bevy_sprite_render = { path = "../bevy_sprite_render", optional = true, version = "0.17.1" } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 4467da12f4f11..76fa11bbaecea 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -29,6 +29,8 @@ plugin_group! { bevy_asset:::AssetPlugin, #[cfg(feature = "bevy_scene")] bevy_scene:::ScenePlugin, + #[cfg(feature = "bevy_scene2")] + bevy_scene2:::ScenePlugin, #[cfg(feature = "bevy_winit")] bevy_winit:::WinitPlugin, #[custom(cfg(all(feature = "dlss", not(feature = "force_disable_dlss"))))] diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 09000b7109c75..4410de0d10400 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -72,6 +72,8 @@ pub use bevy_remote as remote; pub use bevy_render as render; #[cfg(feature = "bevy_scene")] pub use bevy_scene as scene; +#[cfg(feature = "bevy_scene2")] +pub use bevy_scene2 as scene2; #[cfg(feature = "bevy_shader")] pub use bevy_shader as shader; #[cfg(feature = "bevy_solari")] diff --git a/crates/bevy_math/src/rects/rect.rs b/crates/bevy_math/src/rects/rect.rs index 92b7059945949..eda0510a76978 100644 --- a/crates/bevy_math/src/rects/rect.rs +++ b/crates/bevy_math/src/rects/rect.rs @@ -356,6 +356,38 @@ impl Rect { } } + /// Return the area of this rectangle. + /// + /// # Examples + /// + /// ``` + /// # use bevy_math::Rect; + /// let r = Rect::new(0., 0., 10., 10.); // w=10 h=10 + /// assert_eq!(r.area(), 100.0); + /// ``` + #[inline] + pub fn area(&self) -> f32 { + self.width() * self.height() + } + + /// Scale this rect by a multiplicative factor + /// + /// # Examples + /// + /// ``` + /// # use bevy_math::Rect; + /// let r = Rect::new(1., 1., 2., 2.); // w=10 h=10 + /// assert_eq!(r.scale(2.).min.x, 2.0); + /// assert_eq!(r.scale(2.).max.x, 4.0); + /// ``` + #[inline] + pub fn scale(&self, factor: f32) -> Rect { + Self { + min: self.min * factor, + max: self.max * factor, + } + } + /// Returns self as [`IRect`] (i32) #[inline] pub fn as_irect(&self) -> IRect { diff --git a/crates/bevy_mesh/src/components.rs b/crates/bevy_mesh/src/components.rs index 4c5df0496fd78..32136f12373f6 100644 --- a/crates/bevy_mesh/src/components.rs +++ b/crates/bevy_mesh/src/components.rs @@ -37,11 +37,17 @@ use derive_more::derive::From; /// )); /// } /// ``` -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Clone, PartialEq)] #[require(Transform)] pub struct Mesh2d(pub Handle); +impl Default for Mesh2d { + fn default() -> Self { + Self(Handle::default()) + } +} + impl From for AssetId { fn from(mesh: Mesh2d) -> Self { mesh.id() @@ -92,11 +98,17 @@ impl AsAssetId for Mesh2d { /// )); /// } /// ``` -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Clone, PartialEq)] #[require(Transform)] pub struct Mesh3d(pub Handle); +impl Default for Mesh3d { + fn default() -> Self { + Self(Handle::default()) + } +} + impl From for AssetId { fn from(mesh: Mesh3d) -> Self { mesh.id() diff --git a/crates/bevy_mesh/src/skinning.rs b/crates/bevy_mesh/src/skinning.rs index 53b93f9ff2507..5531288102748 100644 --- a/crates/bevy_mesh/src/skinning.rs +++ b/crates/bevy_mesh/src/skinning.rs @@ -4,14 +4,23 @@ use bevy_math::Mat4; use bevy_reflect::prelude::*; use core::ops::Deref; -#[derive(Component, Debug, Default, Clone, Reflect)] -#[reflect(Component, Default, Debug, Clone)] +#[derive(Component, Debug, Clone, Reflect)] +#[reflect(Component, Debug, Clone)] pub struct SkinnedMesh { pub inverse_bindposes: Handle, #[entities] pub joints: Vec, } +impl Default for SkinnedMesh { + fn default() -> Self { + Self { + inverse_bindposes: Handle::default(), + joints: Default::default(), + } + } +} + impl AsAssetId for SkinnedMesh { type Asset = SkinnedMeshInverseBindposes; diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index f4adb94738c48..7134de7da5429 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -325,7 +325,7 @@ pub(crate) fn pack_lightmap_uv_rect(maybe_rect: Option) -> UVec2 { impl Default for Lightmap { fn default() -> Self { Self { - image: Default::default(), + image: Handle::default(), uv_rect: Rect::new(0.0, 0.0, 1.0, 1.0), bicubic_sampling: false, } diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index ff732d7887548..52bd3cc8d3a1b 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -466,10 +466,16 @@ pub struct RenderWireframeMaterial { pub color: [f32; 4], } -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq)] #[reflect(Component, Default, Clone, PartialEq)] pub struct Mesh3dWireframe(pub Handle); +impl Default for Mesh3dWireframe { + fn default() -> Self { + Self(Handle::default()) + } +} + impl AsAssetId for Mesh3dWireframe { type Asset = WireframeMaterial; diff --git a/crates/bevy_platform/src/hash.rs b/crates/bevy_platform/src/hash.rs index 66814bc583623..a6929f4c2a338 100644 --- a/crates/bevy_platform/src/hash.rs +++ b/crates/bevy_platform/src/hash.rs @@ -104,6 +104,13 @@ impl Clone for Hashed { } } +impl From for Hashed { + #[inline] + fn from(value: V) -> Self { + Self::new(value) + } +} + impl Copy for Hashed {} impl Eq for Hashed {} diff --git a/crates/bevy_post_process/src/auto_exposure/settings.rs b/crates/bevy_post_process/src/auto_exposure/settings.rs index ae359a8a01dd4..ecee10553b239 100644 --- a/crates/bevy_post_process/src/auto_exposure/settings.rs +++ b/crates/bevy_post_process/src/auto_exposure/settings.rs @@ -6,7 +6,6 @@ use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; use bevy_image::Image; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{extract_component::ExtractComponent, view::Hdr}; -use bevy_utils::default; /// Component that enables auto exposure for an HDR-enabled 2d or 3d camera. /// @@ -97,8 +96,8 @@ impl Default for AutoExposure { speed_brighten: 3.0, speed_darken: 1.0, exponential_transition_distance: 1.5, - metering_mask: default(), - compensation_curve: default(), + metering_mask: Handle::default(), + compensation_curve: Handle::default(), } } } diff --git a/crates/bevy_reflect/src/impls/bevy_platform/hash.rs b/crates/bevy_reflect/src/impls/bevy_platform/hash.rs index c2a38dd5f31bc..3fe85255a0567 100644 --- a/crates/bevy_reflect/src/impls/bevy_platform/hash.rs +++ b/crates/bevy_reflect/src/impls/bevy_platform/hash.rs @@ -1,5 +1,7 @@ -use bevy_reflect_derive::impl_type_path; +use bevy_reflect_derive::{impl_reflect_opaque, impl_type_path}; impl_type_path!(::bevy_platform::hash::NoOpHash); impl_type_path!(::bevy_platform::hash::FixedHasher); impl_type_path!(::bevy_platform::hash::PassHash); + +impl_reflect_opaque!(::bevy_platform::hash::Hashed()); diff --git a/crates/bevy_render/src/render_resource/pipeline.rs b/crates/bevy_render/src/render_resource/pipeline.rs index 7a6669831234f..3cfef27934827 100644 --- a/crates/bevy_render/src/render_resource/pipeline.rs +++ b/crates/bevy_render/src/render_resource/pipeline.rs @@ -112,7 +112,7 @@ pub struct RenderPipelineDescriptor { pub zero_initialize_workgroup_memory: bool, } -#[derive(Copy, Clone, Debug, Error)] +#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)] #[error("RenderPipelineDescriptor has no FragmentState configured")] pub struct NoFragmentStateError; @@ -126,7 +126,7 @@ impl RenderPipelineDescriptor { } } -#[derive(Clone, Debug, Eq, PartialEq, Default)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct VertexState { /// The compiled shader module for this stage. pub shader: Handle, @@ -138,8 +138,19 @@ pub struct VertexState { pub buffers: Vec, } +impl Default for VertexState { + fn default() -> Self { + Self { + shader: Handle::default(), + shader_defs: Default::default(), + entry_point: Default::default(), + buffers: Default::default(), + } + } +} + /// Describes the fragment process in a render pipeline. -#[derive(Clone, Debug, PartialEq, Eq, Default)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct FragmentState { /// The compiled shader module for this stage. pub shader: Handle, @@ -151,6 +162,17 @@ pub struct FragmentState { pub targets: Vec>, } +impl Default for FragmentState { + fn default() -> Self { + Self { + shader: Handle::default(), + shader_defs: Default::default(), + entry_point: Default::default(), + targets: Default::default(), + } + } +} + impl FragmentState { pub fn set_target(&mut self, index: usize, target: ColorTargetState) { filling_set_at(&mut self.targets, index, None, Some(target)); @@ -158,7 +180,7 @@ impl FragmentState { } /// Describes a compute pipeline. -#[derive(Clone, Debug, PartialEq, Eq, Default)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ComputePipelineDescriptor { pub label: Option>, pub layout: Vec, @@ -174,6 +196,20 @@ pub struct ComputePipelineDescriptor { pub zero_initialize_workgroup_memory: bool, } +impl Default for ComputePipelineDescriptor { + fn default() -> Self { + Self { + shader: Handle::default(), + label: Default::default(), + layout: Default::default(), + push_constant_ranges: Default::default(), + shader_defs: Default::default(), + entry_point: Default::default(), + zero_initialize_workgroup_memory: Default::default(), + } + } +} + // utility function to set a value at the specified index, extending with // a filler value if the index is out of bounds. fn filling_set_at(vec: &mut Vec, index: usize, filler: T, value: T) { diff --git a/crates/bevy_scene/src/components.rs b/crates/bevy_scene/src/components.rs index 7d4e9aef2dbf9..e1e6c93430737 100644 --- a/crates/bevy_scene/src/components.rs +++ b/crates/bevy_scene/src/components.rs @@ -11,16 +11,28 @@ use crate::{DynamicScene, Scene}; /// Adding this component will spawn the scene as a child of that entity. /// Once it's spawned, the entity will have a [`SceneInstance`](crate::SceneInstance) component. -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Debug, PartialEq, Clone)] #[require(Transform)] #[require(Visibility)] pub struct SceneRoot(pub Handle); +impl Default for SceneRoot { + fn default() -> Self { + Self(Handle::default()) + } +} + /// Adding this component will spawn the scene as a child of that entity. /// Once it's spawned, the entity will have a [`SceneInstance`](crate::SceneInstance) component. -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Debug, PartialEq, Clone)] #[require(Transform)] #[require(Visibility)] pub struct DynamicSceneRoot(pub Handle); + +impl Default for DynamicSceneRoot { + fn default() -> Self { + Self(Handle::default()) + } +} diff --git a/crates/bevy_scene2/Cargo.toml b/crates/bevy_scene2/Cargo.toml new file mode 100644 index 0000000000000..c4307f3a5493e --- /dev/null +++ b/crates/bevy_scene2/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "bevy_scene2" +version = "0.17.1" +edition = "2024" + +[dependencies] +bevy_scene2_macros = { path = "macros", version = "0.17.0-dev" } + +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } + +variadics_please = "1.0" + +[lints] +workspace = true diff --git a/crates/bevy_scene2/macros/Cargo.toml b/crates/bevy_scene2/macros/Cargo.toml new file mode 100644 index 0000000000000..f884eda88d0e9 --- /dev/null +++ b/crates/bevy_scene2/macros/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "bevy_scene2_macros" +version = "0.17.0-dev" +edition = "2024" +description = "Derive implementations for bevy_scene" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[lib] +proc-macro = true + +[dependencies] +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } + +syn = { version = "2.0", features = ["full", "extra-traits"] } +proc-macro2 = "1.0" +quote = "1.0" diff --git a/crates/bevy_scene2/macros/src/bsn/codegen.rs b/crates/bevy_scene2/macros/src/bsn/codegen.rs new file mode 100644 index 0000000000000..5df7f27da0c92 --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/codegen.rs @@ -0,0 +1,484 @@ +use crate::bsn::types::{ + Bsn, BsnConstructor, BsnEntry, BsnFields, BsnInheritedScene, BsnRelatedSceneList, BsnRoot, + BsnSceneListItem, BsnSceneListItems, BsnType, BsnValue, +}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use std::collections::{hash_map::Entry, HashMap}; +use syn::{Ident, Index, Lit, Member, Path}; + +#[derive(Default)] +pub(crate) struct EntityRefs { + refs: HashMap, + next_index: usize, +} + +impl EntityRefs { + fn get(&mut self, name: String) -> usize { + match self.refs.entry(name) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let index = self.next_index; + entry.insert(index); + self.next_index += 1; + index + } + } + } +} + +impl BsnRoot { + pub fn to_tokens( + &self, + bevy_scene: &Path, + bevy_ecs: &Path, + bevy_asset: &Path, + entity_refs: &mut EntityRefs, + ) -> TokenStream { + self.0 + .to_tokens(bevy_scene, bevy_ecs, bevy_asset, entity_refs) + } +} + +impl Bsn { + pub fn to_tokens( + &self, + bevy_scene: &Path, + bevy_ecs: &Path, + bevy_asset: &Path, + entity_refs: &mut EntityRefs, + ) -> TokenStream { + let mut entries = Vec::with_capacity(self.entries.len()); + for bsn_entry in &self.entries { + entries.push(match bsn_entry { + BsnEntry::TemplatePatch(bsn_type) => { + let mut assignments = Vec::new(); + bsn_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + &mut assignments, + true, + &[Member::Named(Ident::new( + "value", + proc_macro2::Span::call_site(), + ))], + true, + entity_refs, + ); + let path = &bsn_type.path; + quote! { + <#path as #bevy_scene::PatchTemplate>::patch_template(move |value, _context| { + #(#assignments)* + }) + } + } + BsnEntry::GetTemplatePatch(bsn_type) => { + let mut assignments = Vec::new(); + bsn_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + &mut assignments, + true, + &[Member::Named(Ident::new( + "value", + proc_macro2::Span::call_site(), + ))], + true, + entity_refs, + ); + let path = &bsn_type.path; + quote! { + <#path as #bevy_scene::PatchGetTemplate>::patch(move |value, _context| { + #(#assignments)* + }) + } + } + BsnEntry::TemplateConst { + type_path, + const_ident, + } => { + quote! { + <#type_path as #bevy_scene::PatchTemplate>::patch_template( + move |value, _context| { + *value = #type_path::#const_ident; + }, + ) + } + } + BsnEntry::SceneExpression(block) => { + quote! {#block} + } + BsnEntry::TemplateConstructor(BsnConstructor { + type_path, + function, + args, + }) => { + quote! { + <#type_path as #bevy_scene::PatchTemplate>::patch_template( + move |value, _context| { + *value = #type_path::#function(#args); + }, + ) + } + } + BsnEntry::GetTemplateConstructor(BsnConstructor { + type_path, + function, + args, + }) => { + // NOTE: The odd turbofish line break below avoids breaking rustfmt + quote! { + <#type_path as #bevy_scene::PatchGetTemplate>::patch( + move |value, _context| { + *value = <#type_path as #bevy_ecs::template::GetTemplate> + ::Template::#function(#args); + } + ) + } + } + BsnEntry::ChildrenSceneList(scene_list) => { + let scenes = + scene_list + .0 + .to_tokens(bevy_scene, bevy_ecs, bevy_asset, entity_refs); + quote! { + #bevy_scene::RelatedScenes::<#bevy_ecs::hierarchy::ChildOf, _>::new(#scenes) + } + } + BsnEntry::RelatedSceneList(BsnRelatedSceneList { + scene_list, + relationship_path, + }) => { + let scenes = + scene_list + .0 + .to_tokens(bevy_scene, bevy_ecs, bevy_asset, entity_refs); + // NOTE: The odd turbofish line breaks below avoid breaking rustfmt + quote! { + #bevy_scene::RelatedScenes::< + <#relationship_path as #bevy_ecs::relationship::RelationshipTarget> + ::Relationship, + _ + >::new( + #scenes + ) + } + } + BsnEntry::InheritedScene(inherited_scene) => match inherited_scene { + BsnInheritedScene::Asset(lit_str) => { + quote! {#bevy_scene::InheritSceneAsset::from(#lit_str)} + } + BsnInheritedScene::Fn { function, args } => { + quote! {#bevy_scene::InheritScene(#function(#args))} + } + }, + BsnEntry::Name(ident) => { + let name = ident.to_string(); + let index = entity_refs.get(name.clone()); + quote! { + #bevy_scene::NameEntityReference { + name: Name(#name.into()), + index: #index, + } + } + } + BsnEntry::NameExpression(expr_tokens) => { + quote! { + <#bevy_ecs::name::Name as PatchGetTemplate>::patch( + move |value, _context| { + *value = Name({#expr_tokens}.into()); + } + ) + } + } + }); + } + + quote! {(#(#entries,)*)} + } +} + +macro_rules! field_value_type { + () => { + BsnValue::Expr(_) + | BsnValue::Closure(_) + | BsnValue::Ident(_) + | BsnValue::Lit(_) + | BsnValue::Tuple(_) + }; +} + +impl BsnType { + fn to_patch_tokens( + &self, + bevy_ecs: &Path, + bevy_scene: &Path, + assignments: &mut Vec, + is_root_template: bool, + field_path: &[Member], + is_path_ref: bool, + entity_refs: &mut EntityRefs, + ) { + let path = &self.path; + if !is_root_template { + assignments.push(quote! {#bevy_scene::touch_type::<#path>();}); + } + let maybe_deref = is_path_ref.then(|| quote! {*}); + let maybe_borrow_mut = (!is_path_ref).then(|| quote! {&mut}); + if let Some(variant) = &self.enum_variant { + let variant_name_lower = variant.to_string().to_lowercase(); + let variant_default_name = format_ident!("default_{}", variant_name_lower); + match &self.fields { + BsnFields::Named(fields) => { + let field_assignments = fields.iter().map(|f| { + let name = &f.name; + let value = &f.value; + if let Some(BsnValue::Type(bsn_type)) = &value { + if bsn_type.enum_variant.is_some() { + quote! {*#name = #bsn_type;} + } else { + let mut type_assignments = Vec::new(); + bsn_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + &mut type_assignments, + false, + &[Member::Named(name.clone())], + true, + entity_refs, + ); + quote! {#(#type_assignments)*} + } + } else { + quote! {*#name = #value;} + } + }); + let field_names = fields.iter().map(|f| &f.name); + assignments.push(quote! { + if !matches!(#(#field_path).*, #bevy_ecs::template::Wrapper::<<#path as #bevy_ecs::template::GetTemplate>::Template>::#variant { .. }) { + #maybe_deref #(#field_path).* = #bevy_ecs::template::Wrapper::<<#path as #bevy_ecs::template::GetTemplate>::Template>::#variant_default_name(); + } + if let #bevy_ecs::template::Wrapper::<<#path as #bevy_ecs::template::GetTemplate>::Template>::#variant { #(#field_names, )*.. } = #maybe_borrow_mut #(#field_path).* { + #(#field_assignments)* + } + }) + } + BsnFields::Tuple(fields) => { + // root template enums produce per-field "patches", at the cost of requiring the EnumDefaults pattern + let field_assignments = fields.iter().enumerate().map(|(index, f)| { + let name = format_ident!("t{}", index); + let value = &f.value; + if let BsnValue::Type(bsn_type) = &value { + if bsn_type.enum_variant.is_some() { + quote! {*#name = #bsn_type;} + } else { + let mut type_assignments = Vec::new(); + bsn_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + &mut type_assignments, + false, + &[Member::Named(name.clone())], + true, + entity_refs, + ); + quote! {#(#type_assignments)*} + } + } else { + quote! {*#name = #value;} + } + }); + let field_names = fields + .iter() + .enumerate() + .map(|(index, _)| format_ident!("t{}", index)); + assignments.push(quote! { + if !matches!(#(#field_path).*, #bevy_ecs::template::Wrapper::<<#path as #bevy_ecs::template::GetTemplate>::Template>::#variant(..)) { + #maybe_deref #(#field_path).* = #bevy_ecs::template::Wrapper::<<#path as #bevy_ecs::template::GetTemplate>::Template>::#variant_default_name(); + } + if let #bevy_ecs::template::Wrapper::<<#path as #bevy_ecs::template::GetTemplate>::Template>::#variant (#(#field_names, )*.. ) = #maybe_borrow_mut #(#field_path).* { + #(#field_assignments)* + } + }) + } + } + } else { + match &self.fields { + BsnFields::Named(fields) => { + for field in fields { + let field_name = &field.name; + let field_value = &field.value; + match field_value { + // NOTE: It is very important to still produce outputs for None field values. This is what + // enables field autocomplete in Rust Analyzer + Some(field_value_type!()) | None => { + if field.is_template { + assignments + .push(quote! {#(#field_path.)*#field_name = #bevy_ecs::template::TemplateField::Template(#field_value);}); + } else { + assignments + .push(quote! {#(#field_path.)*#field_name = #field_value;}); + } + } + Some(BsnValue::Name(ident)) => { + let name = ident.to_string(); + let index = entity_refs.get(name); + assignments.push(quote!{ + #(#field_path.)*#field_name = #bevy_ecs::template::EntityReference::Index { scope: _context.current_scope(), index: #index }; + }) + } + Some(BsnValue::Type(field_type)) => { + if field_type.enum_variant.is_some() { + assignments + .push(quote! {#(#field_path.)*#field_name = #field_type;}); + } else { + let mut new_field_path = field_path.to_vec(); + new_field_path.push(Member::Named(field_name.clone())); + field_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + assignments, + false, + &new_field_path, + false, + entity_refs, + ); + } + } + } + } + } + BsnFields::Tuple(fields) => { + for (index, field) in fields.iter().enumerate() { + let field_index = Index::from(index); + let field_value = &field.value; + match field_value { + field_value_type!() => { + if field.is_template { + assignments.push( + quote! {#(#field_path.)*#field_index = #bevy_ecs::template::TemplateField::Template(#field_value);}, + ); + } else { + assignments.push( + quote! {#(#field_path.)*#field_index = #field_value;}, + ); + } + } + BsnValue::Name(ident) => { + let name = ident.to_string(); + let index = entity_refs.get(name); + assignments.push(quote!{ + #(#field_path.)*#field_index = #bevy_ecs::template::EntityReference::Index { scope: _context.current_scope(), index: #index }; + }) + } + BsnValue::Type(field_type) => { + if field_type.enum_variant.is_some() { + assignments + .push(quote! {#(#field_path.)*#field_index = #field_type;}); + } else { + let mut new_field_path = field_path.to_vec(); + new_field_path.push(Member::Unnamed(field_index)); + field_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + assignments, + false, + &new_field_path, + false, + entity_refs, + ); + } + } + } + } + } + } + } + } +} + +impl BsnSceneListItems { + pub fn to_tokens( + &self, + bevy_scene: &Path, + bevy_ecs: &Path, + bevy_asset: &Path, + entity_refs: &mut EntityRefs, + ) -> TokenStream { + let scenes = self.0.iter().map(|scene| match scene { + BsnSceneListItem::Scene(bsn) => { + let tokens = bsn.to_tokens(bevy_scene, bevy_ecs, bevy_asset, entity_refs); + quote! {#bevy_scene::EntityScene(#tokens)} + } + BsnSceneListItem::Expression(statements) => quote! {#(#statements)*}, + }); + quote! { + #bevy_scene::auto_nest_tuple!(#(#scenes),*) + } + } +} + +impl ToTokens for BsnType { + fn to_tokens(&self, tokens: &mut TokenStream) { + let path = &self.path; + let maybe_variant = if let Some(variant) = &self.enum_variant { + Some(quote! {::#variant}) + } else { + None + }; + let result = match &self.fields { + BsnFields::Named(fields) => { + let assignments = fields.iter().map(|f| { + let name = &f.name; + let value = &f.value; + quote! {#name: #value} + }); + quote! { + #path #maybe_variant { + #(#assignments,)* + } + } + } + BsnFields::Tuple(fields) => { + let assignments = fields.iter().map(|f| &f.value); + quote! { + #path #maybe_variant ( + #(#assignments,)* + ) + } + } + }; + result.to_tokens(tokens); + } +} + +impl ToTokens for BsnValue { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + BsnValue::Expr(expr_tokens) => { + quote! {{#expr_tokens}.into()}.to_tokens(tokens); + } + BsnValue::Closure(closure_tokens) => { + quote! {(#closure_tokens).into()}.to_tokens(tokens); + } + BsnValue::Ident(ident) => { + quote! {(#ident).into()}.to_tokens(tokens); + } + BsnValue::Lit(lit) => match lit { + Lit::Str(str) => quote! {#str.into()}.to_tokens(tokens), + _ => lit.to_tokens(tokens), + }, + BsnValue::Tuple(tuple) => { + let tuple_tokens = tuple.0.iter(); + quote! {(#(#tuple_tokens),*)}.to_tokens(tokens); + } + BsnValue::Type(ty) => { + ty.to_tokens(tokens); + } + BsnValue::Name(_ident) => { + // Name requires additional context to convert to tokens + unreachable!() + } + }; + } +} diff --git a/crates/bevy_scene2/macros/src/bsn/mod.rs b/crates/bevy_scene2/macros/src/bsn/mod.rs new file mode 100644 index 0000000000000..3d65533ac84d7 --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/mod.rs @@ -0,0 +1,31 @@ +use crate::bsn::{ + codegen::EntityRefs, + types::{BsnRoot, BsnSceneListItems}, +}; +use bevy_macro_utils::BevyManifest; +use proc_macro::TokenStream; +use syn::parse_macro_input; + +pub mod codegen; +pub mod parse; +pub mod types; + +pub fn bsn(input: TokenStream) -> TokenStream { + let scene = parse_macro_input!(input as BsnRoot); + let manifest = BevyManifest::shared(); + let bevy_scene = manifest.get_path("bevy_scene2"); + let bevy_ecs = manifest.get_path("bevy_ecs"); + let bevy_asset = manifest.get_path("bevy_asset"); + let mut entity_refs = EntityRefs::default(); + TokenStream::from(scene.to_tokens(&bevy_scene, &bevy_ecs, &bevy_asset, &mut entity_refs)) +} + +pub fn bsn_list(input: TokenStream) -> TokenStream { + let scene = parse_macro_input!(input as BsnSceneListItems); + let manifest = BevyManifest::shared(); + let bevy_scene = manifest.get_path("bevy_scene2"); + let bevy_ecs = manifest.get_path("bevy_ecs"); + let bevy_asset = manifest.get_path("bevy_asset"); + let mut entity_refs = EntityRefs::default(); + TokenStream::from(scene.to_tokens(&bevy_scene, &bevy_ecs, &bevy_asset, &mut entity_refs)) +} diff --git a/crates/bevy_scene2/macros/src/bsn/parse.rs b/crates/bevy_scene2/macros/src/bsn/parse.rs new file mode 100644 index 0000000000000..28c2c31c48844 --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/parse.rs @@ -0,0 +1,461 @@ +use crate::bsn::types::{ + Bsn, BsnConstructor, BsnEntry, BsnFields, BsnInheritedScene, BsnNamedField, + BsnRelatedSceneList, BsnRoot, BsnSceneList, BsnSceneListItem, BsnSceneListItems, BsnTuple, + BsnType, BsnUnnamedField, BsnValue, +}; +use proc_macro2::{Delimiter, TokenStream, TokenTree}; +use quote::quote; +use syn::{ + braced, bracketed, + buffer::Cursor, + parenthesized, + parse::{Parse, ParseBuffer, ParseStream}, + spanned::Spanned, + token::{At, Brace, Bracket, Colon, Comma, Paren}, + Block, Expr, Ident, Lit, LitStr, Path, Result, Token, +}; + +/// Functionally identical to [`Punctuated`](syn::punctuated::Punctuated), but fills the given `$list` Vec instead +/// of allocating a new one inside [`Punctuated`](syn::punctuated::Punctuated). This exists to avoid allocating an intermediate Vec. +macro_rules! parse_punctuated_vec { + ($list:ident, $input:ident, $parse:ident, $separator:ident) => { + loop { + if $input.is_empty() { + break; + } + let value = $input.parse::<$parse>()?; + $list.push(value); + if $input.is_empty() { + break; + } + $input.parse::<$separator>()?; + } + }; +} + +impl Parse for BsnRoot { + fn parse(input: ParseStream) -> Result { + Ok(BsnRoot(input.parse::>()?)) + } +} + +impl Parse for Bsn { + fn parse(input: ParseStream) -> Result { + let mut entries = Vec::new(); + if input.peek(Paren) { + let content; + parenthesized![content in input]; + while !content.is_empty() { + entries.push(content.parse::()?); + } + } else { + if ALLOW_FLAT { + while !input.is_empty() { + entries.push(input.parse::()?); + if input.peek(Comma) { + // Not ideal, but this anticipatory break allows us to parse non-parenthesized + // flat Bsn entries in SceneLists + break; + } + } + } else { + entries.push(input.parse::()?); + } + } + + Ok(Self { entries }) + } +} + +impl Parse for BsnEntry { + fn parse(input: ParseStream) -> Result { + Ok(if input.peek(Token![:]) { + BsnEntry::InheritedScene(input.parse::()?) + } else if input.peek(Token![#]) { + input.parse::()?; + if input.peek(Brace) { + BsnEntry::NameExpression(braced_tokens(input)?) + } else { + BsnEntry::Name(input.parse::()?) + } + } else if input.peek(Brace) { + BsnEntry::SceneExpression(braced_tokens(input)?) + } else if input.peek(Bracket) { + BsnEntry::ChildrenSceneList(input.parse::()?) + } else { + let is_template = input.peek(At); + if is_template { + input.parse::()?; + } + let mut path = input.parse::()?; + let path_type = PathType::new(&path); + match path_type { + PathType::Type | PathType::Enum => { + let enum_variant = if matches!(path_type, PathType::Enum) { + take_last_path_ident(&mut path) + } else { + None + }; + if input.peek(Bracket) { + // TODO: fail if this is an enum variant + BsnEntry::RelatedSceneList(BsnRelatedSceneList { + relationship_path: path, + scene_list: input.parse::()?, + }) + } else { + let fields = input.parse::()?; + let bsn_type = BsnType { + path, + enum_variant, + fields, + }; + if is_template { + BsnEntry::TemplatePatch(bsn_type) + } else { + BsnEntry::GetTemplatePatch(bsn_type) + } + } + } + PathType::TypeConst => { + let const_ident = take_last_path_ident(&mut path).unwrap(); + BsnEntry::TemplateConst { + type_path: path, + const_ident, + } + } + PathType::Const => { + todo!("A floating type-unknown const should be assumed to be a const scene right?") + } + PathType::TypeFunction => { + let function = take_last_path_ident(&mut path).unwrap(); + let args = if input.peek(Paren) { + let content; + parenthesized!(content in input); + Some(content.parse_terminated(Expr::parse, Token![,])?) + } else { + None + }; + + let bsn_constructor = BsnConstructor { + type_path: path, + function, + args, + }; + if is_template { + BsnEntry::TemplateConstructor(bsn_constructor) + } else { + BsnEntry::GetTemplateConstructor(bsn_constructor) + } + } + PathType::Function => { + if input.peek(Paren) { + let tokens = parenthesized_tokens(input)?; + BsnEntry::SceneExpression(quote! {#path(#tokens)}) + } else { + BsnEntry::SceneExpression(quote! {#path}) + } + } + } + }) + } +} + +impl Parse for BsnSceneList { + fn parse(input: ParseStream) -> Result { + let content; + bracketed!(content in input); + Ok(BsnSceneList(content.parse::()?)) + } +} + +impl Parse for BsnSceneListItems { + fn parse(input: ParseStream) -> Result { + let mut scenes = Vec::new(); + parse_punctuated_vec!(scenes, input, BsnSceneListItem, Comma); + Ok(BsnSceneListItems(scenes)) + } +} + +impl Parse for BsnSceneListItem { + fn parse(input: ParseStream) -> Result { + Ok(if input.peek(Brace) { + let block = input.parse::()?; + BsnSceneListItem::Expression(block.stmts) + } else { + BsnSceneListItem::Scene(input.parse::>()?) + }) + } +} + +impl Parse for BsnInheritedScene { + fn parse(input: ParseStream) -> Result { + input.parse::()?; + Ok(if input.peek(LitStr) { + let path = input.parse::()?; + BsnInheritedScene::Asset(path) + } else { + let function = input.parse::()?; + let args = if input.peek(Paren) { + let content; + parenthesized!(content in input); + Some(content.parse_terminated(Expr::parse, Token![,])?) + } else { + None + }; + BsnInheritedScene::Fn { function, args } + }) + } +} + +impl Parse for BsnType { + fn parse(input: ParseStream) -> Result { + let mut path = input.parse::()?; + let enum_variant = match PathType::new(&path) { + PathType::Type => None, + PathType::Enum => take_last_path_ident(&mut path), + PathType::Function | PathType::TypeFunction => { + return Err(syn::Error::new( + path.span(), + "Expected a path to a BSN type but encountered a path to a function.", + )) + } + PathType::Const | PathType::TypeConst => { + return Err(syn::Error::new( + path.span(), + "Expected a path to a BSN type but encountered a path to a const.", + )) + } + }; + let fields = input.parse::()?; + Ok(BsnType { + path, + enum_variant, + fields, + }) + } +} + +impl Parse for BsnTuple { + fn parse(input: ParseStream) -> Result { + let content; + parenthesized![content in input]; + let mut fields = Vec::new(); + while !content.is_empty() { + fields.push(content.parse::()?); + } + Ok(BsnTuple(fields)) + } +} + +impl Parse for BsnFields { + fn parse(input: ParseStream) -> Result { + Ok(if input.peek(Brace) { + let content; + braced![content in input]; + let mut fields = Vec::new(); + parse_punctuated_vec!(fields, content, BsnNamedField, Comma); + BsnFields::Named(fields) + } else if input.peek(Paren) { + let content; + parenthesized![content in input]; + let mut fields = Vec::new(); + parse_punctuated_vec!(fields, content, BsnUnnamedField, Comma); + BsnFields::Tuple(fields) + } else { + BsnFields::Named(Vec::new()) + }) + } +} + +impl Parse for BsnNamedField { + fn parse(input: ParseStream) -> Result { + let name = input.parse::()?; + let mut is_template = false; + let value = if input.peek(Colon) { + input.parse::()?; + if input.peek(At) { + input.parse::()?; + is_template = true; + } + Some(input.parse::()?) + } else { + None + }; + Ok(BsnNamedField { + name, + value, + is_template, + }) + } +} + +impl Parse for BsnUnnamedField { + fn parse(input: ParseStream) -> Result { + let mut is_template = false; + if input.peek(At) { + input.parse::()?; + is_template = true; + } + let value = input.parse::()?; + Ok(BsnUnnamedField { value, is_template }) + } +} + +/// Parse a closure "loosely" without caring about the tokens between `|...|` and `{...}`. This ensures autocomplete works. +fn parse_closure_loose<'a>(input: &'a ParseBuffer) -> Result { + let start = input.cursor(); + input.parse::()?; + let tokens = input.step(|cursor| { + let mut rest = *cursor; + while let Some((tt, next)) = rest.token_tree() { + match &tt { + TokenTree::Punct(punct) if punct.as_char() == '|' => { + if let Some((TokenTree::Group(group), next)) = next.token_tree() + && group.delimiter() == Delimiter::Brace + { + return Ok((tokens_between(start, next), next)); + } else { + return Err(cursor.error("closures expect '{' to follow '|'")); + } + } + _ => rest = next, + } + } + Err(cursor.error("no matching `|` was found after this point")) + })?; + Ok(tokens) +} + +// Used to parse a block "loosely" without caring about the content in `{...}`. This ensures autocomplete works. +fn braced_tokens<'a>(input: &'a ParseBuffer) -> Result { + let content; + braced!(content in input); + Ok(content.parse::()?) +} + +// Used to parse parenthesized tokens "loosely" without caring about the content in `(...)`. This ensures autocomplete works. +fn parenthesized_tokens<'a>(input: &'a ParseBuffer) -> Result { + let content; + parenthesized!(content in input); + Ok(content.parse::()?) +} + +fn tokens_between(begin: Cursor, end: Cursor) -> TokenStream { + assert!(begin <= end); + let mut cursor = begin; + let mut tokens = TokenStream::new(); + while cursor < end { + let (token, next) = cursor.token_tree().unwrap(); + tokens.extend(std::iter::once(token)); + cursor = next; + } + tokens +} + +impl Parse for BsnValue { + fn parse(input: ParseStream) -> Result { + Ok(if input.peek(Brace) { + BsnValue::Expr(braced_tokens(input)?) + } else if input.peek(Token![|]) { + let tokens = parse_closure_loose(input)?; + BsnValue::Closure(tokens) + } else if input.peek(Ident) { + let forked = input.fork(); + let path = forked.parse::()?; + if path.segments.len() == 1 && (forked.is_empty() || forked.peek(Comma)) { + return Ok(BsnValue::Ident(input.parse::()?)); + } + match PathType::new(&path) { + PathType::TypeFunction | PathType::Function => { + input.parse::()?; + let token_stream = parenthesized_tokens(input)?; + BsnValue::Expr(quote! { #path(#token_stream) }) + } + PathType::Const | PathType::TypeConst => { + input.parse::()?; + BsnValue::Expr(quote! { #path }) + } + PathType::Type | PathType::Enum => BsnValue::Type(input.parse::()?), + } + } else if input.peek(Lit) { + BsnValue::Lit(input.parse::()?) + } else if input.peek(Paren) { + BsnValue::Tuple(input.parse::()?) + } else if input.peek(Token![#]) { + input.parse::()?; + BsnValue::Name(input.parse::()?) + } else { + return Err(input.error( + "BsnValue parse for this input is not supported yet, nor is proper error handling :)" + )); + }) + } +} + +enum PathType { + Type, + Enum, + Const, + TypeConst, + TypeFunction, + Function, +} + +impl PathType { + fn new(path: &Path) -> PathType { + let mut iter = path.segments.iter().rev(); + if let Some(last_segment) = iter.next() { + let last_string = last_segment.ident.to_string(); + let mut last_string_chars = last_string.chars(); + let last_ident_first_char = last_string_chars.next().unwrap(); + let is_const = last_string_chars + .next() + .map(|last_ident_second_char| last_ident_second_char.is_uppercase()) + .unwrap_or(false); + if last_ident_first_char.is_uppercase() { + if let Some(second_to_last_segment) = iter.next() { + // PERF: is there some way to avoid this string allocation? + let second_to_last_string = second_to_last_segment.ident.to_string(); + let first_char = second_to_last_string.chars().next().unwrap(); + if first_char.is_uppercase() { + if is_const { + PathType::TypeConst + } else { + PathType::Enum + } + } else { + if is_const { + PathType::Const + } else { + PathType::Type + } + } + } else { + PathType::Type + } + } else { + if let Some(second_to_last) = iter.next() { + // PERF: is there some way to avoid this string allocation? + let second_to_last_string = second_to_last.ident.to_string(); + let first_char = second_to_last_string.chars().next().unwrap(); + if first_char.is_uppercase() { + PathType::TypeFunction + } else { + PathType::Function + } + } else { + PathType::Function + } + } + } else { + // This won't be hit so just pick one to make it easy on consumers + PathType::Type + } + } +} + +fn take_last_path_ident(path: &mut Path) -> Option { + let ident = path.segments.pop().map(|s| s.into_value().ident); + path.segments.pop_punct(); + ident +} diff --git a/crates/bevy_scene2/macros/src/bsn/types.rs b/crates/bevy_scene2/macros/src/bsn/types.rs new file mode 100644 index 0000000000000..6fab88e1d024f --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/types.rs @@ -0,0 +1,101 @@ +use proc_macro2::TokenStream; +use syn::{punctuated::Punctuated, Expr, Ident, Lit, LitStr, Path, Stmt, Token}; + +#[derive(Debug)] +pub struct BsnRoot(pub Bsn); + +#[derive(Debug)] +pub struct Bsn { + pub entries: Vec, +} + +#[derive(Debug)] +pub enum BsnEntry { + Name(Ident), + NameExpression(TokenStream), + GetTemplatePatch(BsnType), + TemplatePatch(BsnType), + GetTemplateConstructor(BsnConstructor), + TemplateConstructor(BsnConstructor), + TemplateConst { type_path: Path, const_ident: Ident }, + SceneExpression(TokenStream), + InheritedScene(BsnInheritedScene), + RelatedSceneList(BsnRelatedSceneList), + ChildrenSceneList(BsnSceneList), +} + +#[derive(Debug)] +pub struct BsnType { + pub path: Path, + pub enum_variant: Option, + pub fields: BsnFields, +} + +#[derive(Debug)] +pub struct BsnRelatedSceneList { + pub relationship_path: Path, + pub scene_list: BsnSceneList, +} + +#[derive(Debug)] +pub struct BsnSceneList(pub BsnSceneListItems); + +#[derive(Debug)] +pub struct BsnSceneListItems(pub Vec); + +#[derive(Debug)] +pub enum BsnSceneListItem { + Scene(Bsn), + Expression(Vec), +} + +#[derive(Debug)] +pub enum BsnInheritedScene { + Asset(LitStr), + Fn { + function: Ident, + args: Option>, + }, +} + +#[derive(Debug)] +pub struct BsnConstructor { + pub type_path: Path, + pub function: Ident, + pub args: Option>, +} + +#[derive(Debug)] +pub enum BsnFields { + Named(Vec), + Tuple(Vec), +} + +#[derive(Debug)] +pub struct BsnTuple(pub Vec); + +#[derive(Debug)] +pub struct BsnNamedField { + pub name: Ident, + /// This is an Option to enable autocomplete when the field name is being typed + /// To improve autocomplete further we'll need to forgo a lot of the syn parsing + pub value: Option, + pub is_template: bool, +} + +#[derive(Debug)] +pub struct BsnUnnamedField { + pub value: BsnValue, + pub is_template: bool, +} + +#[derive(Debug)] +pub enum BsnValue { + Expr(TokenStream), + Closure(TokenStream), + Ident(Ident), + Lit(Lit), + Type(BsnType), + Tuple(BsnTuple), + Name(Ident), +} diff --git a/crates/bevy_scene2/macros/src/lib.rs b/crates/bevy_scene2/macros/src/lib.rs new file mode 100644 index 0000000000000..f93d495211687 --- /dev/null +++ b/crates/bevy_scene2/macros/src/lib.rs @@ -0,0 +1,13 @@ +mod bsn; + +use proc_macro::TokenStream; + +#[proc_macro] +pub fn bsn(input: TokenStream) -> TokenStream { + crate::bsn::bsn(input) +} + +#[proc_macro] +pub fn bsn_list(input: TokenStream) -> TokenStream { + crate::bsn::bsn_list(input) +} diff --git a/crates/bevy_scene2/src/lib.rs b/crates/bevy_scene2/src/lib.rs new file mode 100644 index 0000000000000..410324f978bcb --- /dev/null +++ b/crates/bevy_scene2/src/lib.rs @@ -0,0 +1,235 @@ +#![allow(missing_docs)] + +pub mod prelude { + pub use crate::{ + bsn, bsn_list, on, CommandsSpawnScene, LoadScene, PatchGetTemplate, PatchTemplate, Scene, + SceneList, ScenePatchInstance, SpawnRelatedScenes, SpawnScene, + }; +} + +mod resolved_scene; +mod scene; +mod scene_list; +mod scene_patch; +mod spawn; + +pub use bevy_scene2_macros::*; + +pub use resolved_scene::*; +pub use scene::*; +pub use scene_list::*; +pub use scene_patch::*; +pub use spawn::*; + +use bevy_app::{App, Plugin, Update}; +use bevy_asset::{AssetApp, AssetPath, AssetServer, Handle}; +use bevy_ecs::{ + prelude::*, + system::IntoObserverSystem, + template::{Template, TemplateContext}, +}; +use std::marker::PhantomData; + +#[derive(Default)] +pub struct ScenePlugin; + +impl Plugin for ScenePlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::() + .init_asset::() + .init_asset::() + .add_systems(Update, (resolve_scene_patches, spawn_queued).chain()) + .add_observer(on_add_scene_patch_instance); + } +} + +/// This is used by the [`bsn!`] macro to generate compile-time only references to symbols. Currently this is used +/// to add IDE support for nested type names, as it allows us to pass the input Ident from the input to the output code. +pub const fn touch_type() {} + +pub trait LoadScene { + fn load_scene<'a>( + &self, + path: impl Into>, + scene: impl Scene, + ) -> Handle; +} + +impl LoadScene for AssetServer { + fn load_scene<'a>( + &self, + path: impl Into>, + scene: impl Scene, + ) -> Handle { + let scene = ScenePatch::load(self, scene); + self.load_with_path(path, scene) + } +} + +pub struct OnTemplate(pub I, pub PhantomData (E, B, M)>); + +impl + Clone, E: EntityEvent, B: Bundle, M: 'static> Template + for OnTemplate +{ + type Output = (); + + fn build(&mut self, context: &mut TemplateContext) -> Result { + context.entity.observe(self.0.clone()); + Ok(()) + } +} + +impl< + I: IntoObserverSystem + Clone + Send + Sync, + E: EntityEvent, + B: Bundle, + M: 'static, + > Scene for OnTemplate +{ + fn patch(&self, _context: &mut PatchContext, scene: &mut ResolvedScene) { + scene.push_template(OnTemplate(self.0.clone(), PhantomData)); + } +} + +pub fn on, E: EntityEvent, B: Bundle, M: 'static>( + observer: I, +) -> OnTemplate { + OnTemplate(observer, PhantomData) +} + +#[macro_export] +#[doc(hidden)] +macro_rules! auto_nest_tuple { + // direct expansion + () => { () }; + ($a:expr) => { + $a + }; + ($a:expr, $b:expr) => { + ( + $a, + $b, + ) + }; + ($a:expr, $b:expr, $c:expr) => { + ( + $a, + $b, + $c, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr) => { + ( + $a, + $b, + $c, + $d, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + $g, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + $g, + $h, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + $g, + $h, + $i, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + $g, + $h, + $i, + $j, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr, $k:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + $g, + $h, + $i, + $j, + $k, + ) + }; + + // recursive expansion + ( + $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, + $g:expr, $h:expr, $i:expr, $j:expr, $k:expr, $($rest:expr),* + ) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + $g, + $h, + $i, + $j, + $k, + $crate::auto_nest_tuple!($($rest),*) + ) + }; +} diff --git a/crates/bevy_scene2/src/resolved_scene.rs b/crates/bevy_scene2/src/resolved_scene.rs new file mode 100644 index 0000000000000..a882e5c67c471 --- /dev/null +++ b/crates/bevy_scene2/src/resolved_scene.rs @@ -0,0 +1,117 @@ +use bevy_ecs::{ + bundle::Bundle, + entity::Entity, + error::Result, + relationship::Relationship, + template::{ErasedTemplate, Template, TemplateContext}, + world::EntityWorldMut, +}; +use bevy_utils::TypeIdMap; +use std::any::TypeId; + +#[derive(Default)] +pub struct ResolvedScene { + pub template_indices: TypeIdMap, + pub templates: Vec>, + // PERF: special casing children probably makes sense here + pub related: TypeIdMap, + pub entity_references: Vec<(usize, usize)>, +} + +impl std::fmt::Debug for ResolvedScene { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResolvedScene") + .field("related", &self.related) + .field("entity_references", &self.entity_references) + .finish() + } +} + +impl ResolvedScene { + pub fn apply(&mut self, context: &mut TemplateContext) -> Result { + if let Some((scope, index)) = self.entity_references.first().copied() { + context + .scoped_entities + .set(context.entity_scopes, scope, index, context.entity.id()); + } + for template in self.templates.iter_mut() { + template.apply(context)?; + } + + for related in self.related.values_mut() { + let target = context.entity.id(); + context.entity.world_scope(|world| -> Result { + // TODO: I think we need to scan the scene and resolve entities ahead of time, in order to dedupe? Or is there a way to do that + // at patch time? + for scene in &mut related.scenes { + let mut entity = if let Some((scope, index)) = + scene.entity_references.first().copied() + { + let entity = + context + .scoped_entities + .get(world, context.entity_scopes, scope, index); + world.entity_mut(entity) + } else { + world.spawn_empty() + }; + (related.insert)(&mut entity, target); + // PERF: this will result in an archetype move + scene.apply(&mut TemplateContext::new( + &mut entity, + context.scoped_entities, + context.entity_scopes, + ))?; + } + Ok(()) + })?; + } + + Ok(()) + } + + pub fn get_or_insert_template + Default + Send + Sync + 'static>( + &mut self, + ) -> &mut T { + let index = self + .template_indices + .entry(TypeId::of::()) + .or_insert_with(|| { + let index = self.templates.len(); + self.templates.push(Box::new(T::default())); + index + }); + self.templates[*index].downcast_mut::().unwrap() + } + + pub fn push_template + Send + Sync + 'static>( + &mut self, + template: T, + ) { + self.templates.push(Box::new(template)); + } +} + +pub struct ResolvedRelatedScenes { + pub scenes: Vec, + pub insert: fn(&mut EntityWorldMut, target: Entity), +} + +impl std::fmt::Debug for ResolvedRelatedScenes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResolvedRelatedScenes") + .field("scenes", &self.scenes) + .finish() + } +} + +impl ResolvedRelatedScenes { + pub fn new() -> Self { + Self { + scenes: Vec::new(), + insert: |entity, target| { + entity.insert(R::from(target)); + }, + } + } +} diff --git a/crates/bevy_scene2/src/scene.rs b/crates/bevy_scene2/src/scene.rs new file mode 100644 index 0000000000000..b90df67240c64 --- /dev/null +++ b/crates/bevy_scene2/src/scene.rs @@ -0,0 +1,217 @@ +use crate::{ResolvedRelatedScenes, ResolvedScene, SceneList, ScenePatch}; +use bevy_asset::{AssetPath, AssetServer, Assets}; +use bevy_ecs::{ + bundle::Bundle, + error::Result, + name::Name, + relationship::Relationship, + template::{EntityScopes, FnTemplate, GetTemplate, Template, TemplateContext}, +}; +use std::{any::TypeId, marker::PhantomData}; +use variadics_please::all_tuples; + +pub trait Scene: Send + Sync + 'static { + fn patch(&self, context: &mut PatchContext, scene: &mut ResolvedScene); + fn register_dependencies(&self, _dependencies: &mut Vec>) {} +} + +pub struct PatchContext<'a> { + pub assets: &'a AssetServer, + pub patches: &'a Assets, + pub(crate) entity_scopes: &'a mut EntityScopes, + pub(crate) current_scope: usize, +} + +impl<'a> PatchContext<'a> { + #[inline] + pub fn current_scope(&self) -> usize { + self.current_scope + } + + pub fn new_scope(&mut self, func: impl FnOnce(&mut PatchContext)) { + let current_scope = self.entity_scopes.add_scope(); + let mut context = PatchContext { + assets: self.assets, + patches: self.patches, + entity_scopes: self.entity_scopes, + current_scope, + }; + (func)(&mut context); + } +} + +macro_rules! scene_impl { + ($($patch: ident),*) => { + impl<$($patch: Scene),*> Scene for ($($patch,)*) { + fn patch(&self, _context: &mut PatchContext, _scene: &mut ResolvedScene) { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($patch,)*) = self; + $($patch.patch(_context, _scene);)* + } + + fn register_dependencies(&self, _dependencies: &mut Vec>) { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($patch,)*) = self; + $($patch.register_dependencies(_dependencies);)* + } + } + } +} + +all_tuples!(scene_impl, 0, 12, P); + +pub struct TemplatePatch(pub F, pub PhantomData); + +pub fn template_value( + value: T, +) -> TemplatePatch { + TemplatePatch( + move |input: &mut T, _context: &mut PatchContext| { + *input = value.clone(); + }, + PhantomData, + ) +} + +pub trait PatchGetTemplate { + type Template; + fn patch( + func: F, + ) -> TemplatePatch; +} + +impl PatchGetTemplate for G { + type Template = G::Template; + fn patch( + func: F, + ) -> TemplatePatch { + TemplatePatch(func, PhantomData) + } +} + +pub trait PatchTemplate: Sized { + fn patch_template(func: F) -> TemplatePatch; +} + +impl PatchTemplate for T { + fn patch_template(func: F) -> TemplatePatch { + TemplatePatch(func, PhantomData) + } +} + +impl< + F: Fn(&mut T, &mut PatchContext) + Send + Sync + 'static, + T: Template + Send + Sync + Default + 'static, + > Scene for TemplatePatch +{ + fn patch(&self, context: &mut PatchContext, scene: &mut ResolvedScene) { + let template = scene.get_or_insert_template::(); + (self.0)(template, context); + } +} + +pub struct RelatedScenes { + pub related_template_list: L, + pub marker: PhantomData, +} + +impl RelatedScenes { + pub fn new(list: L) -> Self { + Self { + related_template_list: list, + marker: PhantomData, + } + } +} + +impl Scene for RelatedScenes { + fn patch(&self, context: &mut PatchContext, scene: &mut ResolvedScene) { + let related = scene + .related + .entry(TypeId::of::()) + .or_insert_with(ResolvedRelatedScenes::new::); + self.related_template_list + .patch_list(context, &mut related.scenes); + } + + fn register_dependencies(&self, dependencies: &mut Vec>) { + self.related_template_list + .register_dependencies(dependencies); + } +} + +pub struct InheritScene(pub S); + +impl Scene for InheritScene { + fn patch(&self, context: &mut PatchContext, scene: &mut ResolvedScene) { + context.new_scope(|context| { + self.0.patch(context, scene); + }); + } + + fn register_dependencies(&self, dependencies: &mut Vec>) { + self.0.register_dependencies(dependencies); + } +} + +#[derive(Clone)] +pub struct InheritSceneAsset(pub AssetPath<'static>); + +impl>> From for InheritSceneAsset { + fn from(value: I) -> Self { + InheritSceneAsset(value.into()) + } +} + +impl Scene for InheritSceneAsset { + fn patch(&self, context: &mut PatchContext, scene: &mut ResolvedScene) { + let id = context.assets.get_path_id(&self.0).unwrap(); + let scene_patch = context.patches.get(id.typed()).unwrap(); + context.new_scope(|context| { + scene_patch.patch.patch(context, scene); + }); + } + + fn register_dependencies(&self, dependencies: &mut Vec>) { + dependencies.push(self.0.clone()) + } +} + +impl Result) + Clone + Send + Sync + 'static, O: Bundle> Scene + for FnTemplate +{ + fn patch(&self, _context: &mut PatchContext, scene: &mut ResolvedScene) { + scene.push_template(FnTemplate(self.0.clone())); + } +} + +pub struct NameEntityReference { + pub name: Name, + pub index: usize, +} + +impl Scene for NameEntityReference { + fn patch(&self, context: &mut PatchContext, scene: &mut ResolvedScene) { + if let Some((scope, index)) = scene.entity_references.first().copied() { + let entity_index = context.entity_scopes.get(scope, index).unwrap(); + context + .entity_scopes + .assign(context.current_scope, self.index, entity_index); + } else { + context + .entity_scopes + .alloc(context.current_scope, self.index); + } + scene + .entity_references + .push((context.current_scope, self.index)); + let name = scene.get_or_insert_template::(); + *name = self.name.clone(); + } +} diff --git a/crates/bevy_scene2/src/scene_list.rs b/crates/bevy_scene2/src/scene_list.rs new file mode 100644 index 0000000000000..0c4329ef8b56c --- /dev/null +++ b/crates/bevy_scene2/src/scene_list.rs @@ -0,0 +1,81 @@ +use crate::{PatchContext, ResolvedScene, Scene}; +use bevy_asset::AssetPath; +use variadics_please::all_tuples; + +pub trait SceneList: Send + Sync + 'static { + fn patch_list(&self, context: &mut PatchContext, scenes: &mut Vec); + + fn register_dependencies(&self, dependencies: &mut Vec>); +} + +pub struct EntityScene(pub S); + +impl SceneList for EntityScene { + fn patch_list(&self, context: &mut PatchContext, scenes: &mut Vec) { + let mut resolved_scene = ResolvedScene::default(); + self.0.patch(context, &mut resolved_scene); + scenes.push(resolved_scene); + } + + fn register_dependencies(&self, dependencies: &mut Vec>) { + self.0.register_dependencies(dependencies); + } +} + +macro_rules! scene_list_impl { + ($($list: ident),*) => { + impl<$($list: SceneList),*> SceneList for ($($list,)*) { + fn patch_list(&self, _context: &mut PatchContext, _scenes: &mut Vec) { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($list,)*) = self; + $($list.patch_list(_context, _scenes);)* + } + + fn register_dependencies(&self, _dependencies: &mut Vec>) { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($list,)*) = self; + $($list.register_dependencies(_dependencies);)* + } + } + } +} + +all_tuples!(scene_list_impl, 0, 12, P); + +impl SceneList for Vec { + fn patch_list(&self, context: &mut PatchContext, scenes: &mut Vec) { + for scene in self { + let mut resolved_scene = ResolvedScene::default(); + scene.patch(context, &mut resolved_scene); + scenes.push(resolved_scene); + } + } + + fn register_dependencies(&self, dependencies: &mut Vec>) { + for scene in self { + scene.register_dependencies(dependencies); + } + } +} + +impl SceneList for Vec> { + fn patch_list(&self, context: &mut PatchContext, scenes: &mut Vec) { + for scene in self { + let mut resolved_scene = ResolvedScene::default(); + scene.patch(context, &mut resolved_scene); + scenes.push(resolved_scene); + } + } + + fn register_dependencies(&self, dependencies: &mut Vec>) { + for scene in self { + scene.register_dependencies(dependencies); + } + } +} diff --git a/crates/bevy_scene2/src/scene_patch.rs b/crates/bevy_scene2/src/scene_patch.rs new file mode 100644 index 0000000000000..de4e3d7ac4065 --- /dev/null +++ b/crates/bevy_scene2/src/scene_patch.rs @@ -0,0 +1,62 @@ +use crate::{ResolvedScene, Scene, SceneList}; +use bevy_asset::{Asset, AssetServer, Handle, UntypedHandle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{component::Component, template::EntityScopes}; +use bevy_reflect::TypePath; + +#[derive(Asset, TypePath)] +pub struct ScenePatch { + pub patch: Box, + #[dependency] + pub dependencies: Vec, + // TODO: consider breaking this out to prevent mutating asset events when resolved + pub resolved: Option, + pub entity_scopes: Option, +} + +impl ScenePatch { + pub fn load(assets: &AssetServer, scene: P) -> Self { + let mut dependencies = Vec::new(); + scene.register_dependencies(&mut dependencies); + let dependencies = dependencies + .iter() + .map(|i| assets.load::(i.clone()).untyped()) + .collect::>(); + ScenePatch { + patch: Box::new(scene), + dependencies, + resolved: None, + entity_scopes: None, + } + } +} + +#[derive(Component, Deref, DerefMut)] +pub struct ScenePatchInstance(pub Handle); + +#[derive(Asset, TypePath)] +pub struct SceneListPatch { + pub patch: Box, + #[dependency] + pub dependencies: Vec, + // TODO: consider breaking this out to prevent mutating asset events when resolved + pub resolved: Option>, + pub entity_scopes: Option, +} + +impl SceneListPatch { + pub fn load(assets: &AssetServer, scene_list: L) -> Self { + let mut dependencies = Vec::new(); + scene_list.register_dependencies(&mut dependencies); + let dependencies = dependencies + .iter() + .map(|i| assets.load::(i.clone()).untyped()) + .collect::>(); + SceneListPatch { + patch: Box::new(scene_list), + dependencies, + resolved: None, + entity_scopes: None, + } + } +} diff --git a/crates/bevy_scene2/src/spawn.rs b/crates/bevy_scene2/src/spawn.rs new file mode 100644 index 0000000000000..3254abe4584ae --- /dev/null +++ b/crates/bevy_scene2/src/spawn.rs @@ -0,0 +1,300 @@ +use crate::{ + PatchContext, ResolvedScene, Scene, SceneList, SceneListPatch, ScenePatch, ScenePatchInstance, +}; +use bevy_asset::{AssetEvent, AssetServer, Assets, Handle}; +use bevy_ecs::{ + message::MessageCursor, + prelude::*, + relationship::Relationship, + template::{EntityScopes, ScopedEntities, TemplateContext}, +}; +use bevy_platform::collections::HashMap; + +pub trait SpawnScene { + fn spawn_scene(&mut self, scene: S) -> EntityWorldMut<'_>; +} + +pub trait SpawnRelatedScenes { + fn spawn_related_scenes(self, scenes: impl SceneList) -> Self; +} + +impl SpawnScene for World { + fn spawn_scene(&mut self, scene: S) -> EntityWorldMut<'_> { + let assets = self.resource::(); + let patch = ScenePatch::load(assets, scene); + let handle = assets.add(patch); + self.spawn(ScenePatchInstance(handle)) + } +} + +impl SpawnRelatedScenes for EntityWorldMut<'_> { + fn spawn_related_scenes(mut self, scenes: impl SceneList) -> Self { + let assets = self.resource::(); + let patch = SceneListPatch::load(assets, scenes); + let handle = assets.add(patch); + let entity = self.id(); + self.resource_mut::().scene_entities.push(( + SceneListSpawn { + entity, + insert: |entity, target| { + entity.insert( + <::Relationship as Relationship>::from(target), + ); + }, + }, + handle, + )); + self + } +} + +impl SpawnRelatedScenes for EntityCommands<'_> { + fn spawn_related_scenes(mut self, scenes: impl SceneList) -> Self { + self.queue(move |entity: EntityWorldMut| { + entity.spawn_related_scenes::(scenes); + }); + + self + } +} + +pub trait CommandsSpawnScene { + fn spawn_scene(&mut self, scene: S) -> EntityCommands<'_>; +} + +impl<'w, 's> CommandsSpawnScene for Commands<'w, 's> { + fn spawn_scene(&mut self, scene: S) -> EntityCommands<'_> { + let mut entity_commands = self.spawn_empty(); + let id = entity_commands.id(); + entity_commands.commands().queue(move |world: &mut World| { + let assets = world.resource::(); + let patch = ScenePatch::load(assets, scene); + let handle = assets.add(patch); + if let Ok(mut entity) = world.get_entity_mut(id) { + entity.insert(ScenePatchInstance(handle)); + } + }); + entity_commands + } +} + +pub fn resolve_scene_patches( + mut events: MessageReader>, + mut list_events: MessageReader>, + assets: Res, + mut patches: ResMut>, + mut list_patches: ResMut>, +) { + for event in events.read() { + match *event { + // TODO: handle modified? + AssetEvent::LoadedWithDependencies { id } => { + let mut scene = ResolvedScene::default(); + let mut entity_scopes = EntityScopes::default(); + // TODO: real error handling + let patch = patches.get(id).unwrap(); + patch.patch.patch( + &mut PatchContext { + assets: &assets, + patches: &patches, + current_scope: entity_scopes.add_scope(), + entity_scopes: &mut entity_scopes, + }, + &mut scene, + ); + let patch = patches.get_mut(id).unwrap(); + patch.resolved = Some(scene); + patch.entity_scopes = Some(entity_scopes); + } + _ => {} + } + } + for event in list_events.read() { + match *event { + // TODO: handle modified? + AssetEvent::LoadedWithDependencies { id } => { + let list_patch = list_patches.get_mut(id).unwrap(); + let mut entity_scopes = EntityScopes::default(); + let mut scenes = Vec::new(); + // TODO: real error handling + list_patch.patch.patch_list( + &mut PatchContext { + assets: &assets, + patches: &patches, + current_scope: entity_scopes.add_scope(), + entity_scopes: &mut entity_scopes, + }, + &mut scenes, + ); + list_patch.resolved = Some(scenes); + list_patch.entity_scopes = Some(entity_scopes); + } + _ => {} + } + } +} + +#[derive(Resource, Default)] +pub struct QueuedScenes { + waiting_entities: HashMap, Vec>, + waiting_list_entities: HashMap, Vec>, +} + +struct SceneListSpawn { + entity: Entity, + insert: fn(&mut EntityWorldMut, target: Entity), +} + +#[derive(Resource, Default)] +pub struct NewScenes { + entities: Vec, + scene_entities: Vec<(SceneListSpawn, Handle)>, +} + +pub fn on_add_scene_patch_instance( + add: On, + mut new_scenes: ResMut, +) { + new_scenes.entities.push(add.entity); +} + +pub fn spawn_queued( + world: &mut World, + handles: &mut QueryState<&ScenePatchInstance>, + mut reader: Local>>, + mut list_reader: Local>>, +) { + world.resource_scope(|world, mut patches: Mut>| { + world.resource_scope(|world, mut list_patches: Mut>| { + world.resource_scope(|world, mut queued: Mut| { + world.resource_scope(|world, events: Mut>>| { + world.resource_scope( + |world, list_events: Mut>>| { + loop { + let mut new_scenes = world.resource_mut::(); + if new_scenes.entities.is_empty() { + break; + } + for entity in core::mem::take(&mut new_scenes.entities) { + if let Ok(handle) = handles.get(world, entity).map(|h| &h.0) { + if let Some((Some(scene), Some(entity_scopes))) = + patches.get_mut(handle).map(|p| { + (p.resolved.as_mut(), p.entity_scopes.as_ref()) + }) + { + let mut entity_mut = + world.get_entity_mut(entity).unwrap(); + scene + .apply(&mut TemplateContext::new( + &mut entity_mut, + &mut ScopedEntities::new( + entity_scopes.entity_count(), + ), + entity_scopes, + )) + .unwrap(); + } else { + let entities = queued + .waiting_entities + .entry(handle.clone()) + .or_default(); + entities.push(entity); + } + } + } + } + loop { + let mut new_scenes = world.resource_mut::(); + if new_scenes.scene_entities.is_empty() { + break; + } + for (scene_list_spawn, handle) in + core::mem::take(&mut new_scenes.scene_entities) + { + if let Some((Some(resolved_scenes), Some(entity_scopes))) = + list_patches.get_mut(&handle).map(|p| { + (p.resolved.as_mut(), p.entity_scopes.as_ref()) + }) + { + for scene in resolved_scenes { + let mut child_entity = world.spawn_empty(); + (scene_list_spawn.insert)( + &mut child_entity, + scene_list_spawn.entity, + ); + scene + .apply(&mut TemplateContext::new( + &mut child_entity, + &mut ScopedEntities::new( + entity_scopes.entity_count(), + ), + entity_scopes, + )) + .unwrap(); + } + } else { + let entities = + queued.waiting_list_entities.entry(handle).or_default(); + entities.push(scene_list_spawn); + } + } + } + + for event in reader.read(&events) { + if let AssetEvent::LoadedWithDependencies { id } = event + && let Some((Some(scene), Some(entity_scopes))) = patches + .get_mut(*id) + .map(|p| (p.resolved.as_mut(), p.entity_scopes.as_ref())) + && let Some(entities) = queued.waiting_entities.remove(id) + { + for entity in entities { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + scene + .apply(&mut TemplateContext::new( + &mut entity_mut, + &mut ScopedEntities::new( + entity_scopes.entity_count(), + ), + entity_scopes, + )) + .unwrap(); + } + } + } + } + for event in list_reader.read(&list_events) { + if let AssetEvent::LoadedWithDependencies { id } = event + && let Some((Some(resolved_scenes), Some(entity_scopes))) = + list_patches.get_mut(*id).map(|p| { + (p.resolved.as_mut(), p.entity_scopes.as_ref()) + }) + && let Some(scene_list_spawns) = + queued.waiting_list_entities.remove(id) + { + for scene_list_spawn in scene_list_spawns { + for scene in resolved_scenes.iter_mut() { + let mut child_entity = world.spawn_empty(); + (scene_list_spawn.insert)( + &mut child_entity, + scene_list_spawn.entity, + ); + scene + .apply(&mut TemplateContext::new( + &mut child_entity, + &mut ScopedEntities::new( + entity_scopes.entity_count(), + ), + entity_scopes, + )) + .unwrap(); + } + } + } + } + }, + ); + }); + }); + }); + }); +} diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index c4e4469dc2178..31e889266ade4 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -11,7 +11,7 @@ use bevy_transform::components::Transform; use crate::TextureSlicer; /// Describes a sprite to be rendered to a 2D camera -#[derive(Component, Debug, Default, Clone, Reflect)] +#[derive(Component, Debug, Clone, Reflect)] #[require(Transform, Visibility, VisibilityClass, Anchor)] #[reflect(Component, Default, Debug, Clone)] #[component(on_add = visibility::add_visibility_class::)] @@ -39,6 +39,21 @@ pub struct Sprite { pub image_mode: SpriteImageMode, } +impl Default for Sprite { + fn default() -> Self { + Self { + image: Handle::default(), + texture_atlas: Default::default(), + color: Default::default(), + flip_x: Default::default(), + flip_y: Default::default(), + custom_size: Default::default(), + rect: Default::default(), + image_mode: Default::default(), + } + } +} + impl Sprite { /// Create a Sprite with a custom size pub fn sized(custom_size: Vec2) -> Self { diff --git a/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs index 98ab9c73f3fae..da77ca08f2419 100644 --- a/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs @@ -455,10 +455,16 @@ pub struct RenderWireframeMaterial { pub color: [f32; 4], } -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq)] #[reflect(Component, Default, Clone, PartialEq)] pub struct Mesh2dWireframe(pub Handle); +impl Default for Mesh2dWireframe { + fn default() -> Self { + Self(Handle::default()) + } +} + impl AsAssetId for Mesh2dWireframe { type Asset = Wireframe2dMaterial; diff --git a/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs index 5139ab704b5a6..1d5ecd94656fd 100644 --- a/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs +++ b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs @@ -46,7 +46,7 @@ pub struct TilemapChunkMeshCache(HashMap>); /// A component representing a chunk of a tilemap. /// Each chunk is a rectangular section of tiles that is rendered as a single mesh. -#[derive(Component, Clone, Debug, Default, Reflect)] +#[derive(Component, Clone, Debug, Reflect)] #[reflect(Component, Clone, Debug, Default)] #[component(immutable, on_insert = on_insert_tilemap_chunk)] pub struct TilemapChunk { @@ -61,6 +61,17 @@ pub struct TilemapChunk { pub alpha_mode: AlphaMode2d, } +impl Default for TilemapChunk { + fn default() -> Self { + Self { + chunk_size: Default::default(), + tile_display_size: Default::default(), + tileset: Handle::default(), + alpha_mode: Default::default(), + } + } +} + impl TilemapChunk { pub fn calculate_tile_transform(&self, position: UVec2) -> Transform { Transform::from_xyz( diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index e4da3288d43c0..392a9485d292c 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -322,7 +322,7 @@ impl From for TextFont { impl Default for TextFont { fn default() -> Self { Self { - font: Default::default(), + font: Handle::default(), font_size: 20.0, line_height: LineHeight::default(), font_smoothing: Default::default(), diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs index a5793ca778a57..78af652518984 100644 --- a/crates/bevy_ui/src/interaction_states.rs +++ b/crates/bevy_ui/src/interaction_states.rs @@ -45,7 +45,7 @@ pub struct Pressed; pub struct Checkable; /// Component that indicates whether a checkbox or radio button is in a checked state. -#[derive(Component, Default, Debug)] +#[derive(Component, Default, Debug, Clone)] pub struct Checked; pub(crate) fn on_add_checkable(add: On, mut world: DeferredWorld) { diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index cb29d5b07fafe..8100d72b16eb7 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -2244,7 +2244,7 @@ pub struct CalculatedClip { /// UI node entities with this component will ignore any clipping rect they inherit, /// the node will not be clipped regardless of its ancestors' `Overflow` setting. -#[derive(Component)] +#[derive(Component, Default, Clone)] pub struct OverrideClip; #[expect( diff --git a/crates/bevy_ui_widgets/Cargo.toml b/crates/bevy_ui_widgets/Cargo.toml index 38ae0753f6a6a..f2585fdfbbdd1 100644 --- a/crates/bevy_ui_widgets/Cargo.toml +++ b/crates/bevy_ui_widgets/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["bevy"] # bevy bevy_app = { path = "../bevy_app", version = "0.17.1" } bevy_a11y = { path = "../bevy_a11y", version = "0.17.1" } +bevy_camera = { path = "../bevy_camera", version = "0.17.1" } bevy_ecs = { path = "../bevy_ecs", version = "0.17.1" } bevy_input = { path = "../bevy_input", version = "0.17.1" } bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.1" } diff --git a/crates/bevy_ui_widgets/src/button.rs b/crates/bevy_ui_widgets/src/button.rs index 4c98f6063d8d6..6ff43a7f6a29a 100644 --- a/crates/bevy_ui_widgets/src/button.rs +++ b/crates/bevy_ui_widgets/src/button.rs @@ -20,7 +20,7 @@ use crate::Activate; /// Headless button widget. This widget maintains a "pressed" state, which is used to /// indicate whether the button is currently being pressed by the user. It emits an [`Activate`] /// event when the button is un-pressed. -#[derive(Component, Default, Debug)] +#[derive(Component, Clone, Debug, Default)] #[require(AccessibilityNode(accesskit::Node::new(Role::Button)))] pub struct Button; diff --git a/crates/bevy_ui_widgets/src/checkbox.rs b/crates/bevy_ui_widgets/src/checkbox.rs index f4620c15049b2..e11232ebc9f36 100644 --- a/crates/bevy_ui_widgets/src/checkbox.rs +++ b/crates/bevy_ui_widgets/src/checkbox.rs @@ -29,7 +29,7 @@ use bevy_ecs::entity::Entity; /// The [`Checkbox`] component can be used to implement other kinds of toggle widgets. If you /// are going to do a toggle switch, you should override the [`AccessibilityNode`] component with /// the `Switch` role instead of the `Checkbox` role. -#[derive(Component, Debug, Default)] +#[derive(Component, Debug, Default, Clone)] #[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)] pub struct Checkbox; diff --git a/crates/bevy_ui_widgets/src/lib.rs b/crates/bevy_ui_widgets/src/lib.rs index 3ab239412775e..8f30e2842701a 100644 --- a/crates/bevy_ui_widgets/src/lib.rs +++ b/crates/bevy_ui_widgets/src/lib.rs @@ -20,13 +20,16 @@ mod button; mod checkbox; +mod menu; mod observe; +pub mod popover; mod radio; mod scrollbar; mod slider; pub use button::*; pub use checkbox::*; +pub use menu::*; pub use observe::*; pub use radio::*; pub use scrollbar::*; @@ -35,6 +38,8 @@ pub use slider::*; use bevy_app::{PluginGroup, PluginGroupBuilder}; use bevy_ecs::{entity::Entity, event::EntityEvent}; +use crate::popover::PopoverPlugin; + /// A plugin group that registers the observers for all of the widgets in this crate. If you don't want to /// use all of the widgets, you can import the individual widget plugins instead. pub struct UiWidgetsPlugins; @@ -42,8 +47,10 @@ pub struct UiWidgetsPlugins; impl PluginGroup for UiWidgetsPlugins { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::() + .add(PopoverPlugin) .add(ButtonPlugin) .add(CheckboxPlugin) + .add(MenuPlugin) .add(RadioGroupPlugin) .add(ScrollbarPlugin) .add(SliderPlugin) diff --git a/crates/bevy_ui_widgets/src/menu.rs b/crates/bevy_ui_widgets/src/menu.rs new file mode 100644 index 0000000000000..098ba81a3ff2c --- /dev/null +++ b/crates/bevy_ui_widgets/src/menu.rs @@ -0,0 +1,377 @@ +//! Core widget components for menus and menu buttons. + +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::{ + component::Component, + entity::Entity, + event::EntityEvent, + hierarchy::ChildOf, + observer::On, + query::{Has, With, Without}, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res, ResMut}, + template::GetTemplate, +}; +use bevy_input::{ + keyboard::{KeyCode, KeyboardInput}, + ButtonState, +}; +use bevy_input_focus::{ + tab_navigation::{NavAction, TabGroup, TabNavigation}, + FocusedInput, InputFocus, +}; +use bevy_log::warn; +use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; +use bevy_ui::{InteractionDisabled, Pressed}; + +use crate::Activate; + +/// Action type for [`MenuEvent`]. +#[derive(Clone, Copy, Debug)] +pub enum MenuAction { + /// Indicates we want to open the menu, if it is not already open. + Open, + /// Open the menu if it's closed, close it if it's open. Generally sent from a menu button. + Toggle, + /// Close the menu and despawn it. Despawning may not happen immediately if there is a closing + /// transition animation. + Close, + /// Close the entire menu stack. + CloseAll, + /// Set focus to the menu button or other owner of the popup stack. This happens when + /// the escape key is pressed. + FocusRoot, +} + +/// Event used to control the state of the open menu. This bubbles upwards from the menu items +/// and the menu container, through the portal relation, and to the menu owner entity. +/// +/// Focus navigation: the menu may be part of a composite of multiple menus such as a menu bar. +/// This means that depending on direction, focus movement may move to the next menu item, or +/// the next menu. This also means that different events will often be handled at different +/// levels of the hierarchy - some being handled by the popup, and some by the popup's owner. +#[derive(EntityEvent, Clone, Debug)] +#[entity_event(propagate, auto_propagate)] +pub struct MenuEvent { + /// The [`MenuItem`] or [`MenuPopup`] that triggered this event. + #[event_target] + pub source: Entity, + + /// The desired action in response to this event. + pub action: MenuAction, +} + +/// Specifies the layout direction of the menu, for keyboard navigation +#[derive(Default, Debug, Clone, PartialEq)] +pub enum MenuLayout { + /// A vertical stack. Up and down arrows to move between items. + #[default] + Column, + /// A horizontal row. Left and right arrows to move between items. + Row, + /// A 2D grid. Arrow keys are not mapped, you'll need to write your own observer. + Grid, +} + +/// Component that defines a popup menu container. +/// +/// A popup menu *must* contain at least one focusable entity. The first such entity will acquire +/// focus when the popup is spawned; arrow keys can be used to navigate between menu items. If no +/// descendant of the menu has focus, the menu will automatically close. This rule has several +/// consequences: +/// +/// * Clicking on another widget or empty space outside the menu will cause the menu to close. +/// * Two menus cannot be displayed at the same time unless one is an ancestor of the other. +#[derive(Component, Debug, Default, Clone)] +#[require( + AccessibilityNode(accesskit::Node::new(Role::MenuListPopup)), + TabGroup::modal() +)] +#[require(MenuAcquireFocus)] +pub struct MenuPopup { + /// The layout orientation of the menu + pub layout: MenuLayout, +} + +/// Component that defines a menu item. +#[derive(Component, Debug, Clone, GetTemplate)] +#[require(AccessibilityNode(accesskit::Node::new(Role::MenuItem)))] +pub struct MenuItem; + +/// Marker component that indicates that we need to set focus to the first menu item. +#[derive(Component, Debug, Default)] +struct MenuAcquireFocus; + +/// Component that indicates that the menu is closing. +#[derive(Component, Debug, Default)] +struct MenuClosing; + +fn menu_acquire_focus( + q_menus: Query, With)>, + mut focus: ResMut, + tab_navigation: TabNavigation, + mut commands: Commands, +) { + for menu in q_menus.iter() { + // When a menu is spawned, attempt to find the first focusable menu item, and set focus + // to it. + match tab_navigation.initialize(menu, NavAction::First) { + Ok(next) => { + commands.entity(menu).remove::(); + focus.0 = Some(next); + } + Err(e) => { + warn!( + "No focusable menu items for popup menu: {}, error: {:?}", + menu, e + ); + } + } + } +} + +fn menu_on_lose_focus( + q_menus: Query< + Entity, + ( + With, + Without, + Without, + ), + >, + q_parent: Query<&ChildOf>, + focus: Res, + mut commands: Commands, +) { + // Close any menu which doesn't contain the focus entity. + for menu in q_menus.iter() { + // TODO: Change this logic when we support submenus. Don't want to send multiple close + // events. Perhaps what we can do is add `CoreMenuClosing` to the whole stack. + let contains_focus = match focus.0 { + Some(focus_ent) => { + focus_ent == menu || q_parent.iter_ancestors(focus_ent).any(|ent| ent == menu) + } + None => false, + }; + + if !contains_focus { + commands.entity(menu).insert(MenuClosing); + commands.trigger(MenuEvent { + source: menu, + action: MenuAction::CloseAll, + }); + } + } +} + +fn menu_on_key_event( + mut ev: On>, + q_item: Query, With>, + q_menu: Query<&MenuPopup>, + tab_navigation: TabNavigation, + mut focus: ResMut, + mut commands: Commands, +) { + if let Ok(disabled) = q_item.get(ev.focused_entity) { + if !disabled { + let event = &ev.event().input; + if !event.repeat && event.state == ButtonState::Pressed { + match event.key_code { + // Activate the item and close the popup + KeyCode::Enter | KeyCode::Space => { + ev.propagate(false); + // Trigger the menu action + commands.trigger(Activate { + entity: ev.event().focused_entity, + }); + // Set the focus to the menu button. + commands.trigger(MenuEvent { + source: ev.event().focused_entity, + action: MenuAction::FocusRoot, + }); + // Close the stack + commands.trigger(MenuEvent { + source: ev.event().focused_entity, + action: MenuAction::CloseAll, + }); + } + + _ => (), + } + } + } + } else if let Ok(menu) = q_menu.get(ev.focused_entity) { + let event = &ev.event().input; + if !event.repeat && event.state == ButtonState::Pressed { + match event.key_code { + // Close the popup + KeyCode::Escape => { + ev.propagate(false); + // Set the focus to the menu button. + commands.trigger(MenuEvent { + source: ev.focused_entity, + action: MenuAction::FocusRoot, + }); + // Close the stack + commands.trigger(MenuEvent { + source: ev.focused_entity, + action: MenuAction::CloseAll, + }); + } + + // Focus the adjacent item in the up direction + KeyCode::ArrowUp => { + if menu.layout == MenuLayout::Column { + ev.propagate(false); + focus.0 = tab_navigation.navigate(&focus, NavAction::Previous).ok(); + } + } + + // Focus the adjacent item in the down direction + KeyCode::ArrowDown => { + if menu.layout == MenuLayout::Column { + ev.propagate(false); + focus.0 = tab_navigation.navigate(&focus, NavAction::Next).ok(); + } + } + + // Focus the adjacent item in the left direction + KeyCode::ArrowLeft => { + if menu.layout == MenuLayout::Row { + ev.propagate(false); + focus.0 = tab_navigation.navigate(&focus, NavAction::Previous).ok(); + } + } + + // Focus the adjacent item in the right direction + KeyCode::ArrowRight => { + if menu.layout == MenuLayout::Row { + ev.propagate(false); + focus.0 = tab_navigation.navigate(&focus, NavAction::Next).ok(); + } + } + + // Focus the first item + KeyCode::Home => { + ev.propagate(false); + focus.0 = tab_navigation.navigate(&focus, NavAction::First).ok(); + } + + // Focus the last item + KeyCode::End => { + ev.propagate(false); + focus.0 = tab_navigation.navigate(&focus, NavAction::Last).ok(); + } + + _ => (), + } + } + } +} + +fn menu_item_on_pointer_click( + mut ev: On>, + mut q_state: Query<(Has, Has), With>, + mut commands: Commands, +) { + if let Ok((pressed, disabled)) = q_state.get_mut(ev.entity) { + ev.propagate(false); + if pressed && !disabled { + // Trigger the menu action. + commands.trigger(Activate { entity: ev.entity }); + // Set the focus to the menu button. + commands.trigger(MenuEvent { + source: ev.entity, + action: MenuAction::FocusRoot, + }); + // Close the stack + commands.trigger(MenuEvent { + source: ev.entity, + action: MenuAction::CloseAll, + }); + } + } +} + +fn menu_item_on_pointer_down( + mut ev: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((item, disabled, pressed)) = q_state.get_mut(ev.entity) { + ev.propagate(false); + if !disabled && !pressed { + commands.entity(item).insert(Pressed); + } + } +} + +fn menu_item_on_pointer_up( + mut ev: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((item, disabled, pressed)) = q_state.get_mut(ev.entity) { + ev.propagate(false); + if !disabled && pressed { + commands.entity(item).remove::(); + } + } +} + +fn menu_item_on_pointer_drag_end( + mut ev: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((item, disabled, pressed)) = q_state.get_mut(ev.entity) { + ev.propagate(false); + if !disabled && pressed { + commands.entity(item).remove::(); + } + } +} + +fn menu_item_on_pointer_cancel( + mut ev: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((item, disabled, pressed)) = q_state.get_mut(ev.entity) { + ev.propagate(false); + if !disabled && pressed { + commands.entity(item).remove::(); + } + } +} + +fn menu_on_menu_event( + mut ev: On, + q_popup: Query<(), With>, + mut commands: Commands, +) { + if q_popup.contains(ev.source) { + if let MenuAction::Close = ev.event().action { + ev.propagate(false); + commands.entity(ev.source).despawn(); + } + } +} + +/// Plugin that adds the observers for the [`CoreMenuItem`] widget. +pub struct MenuPlugin; + +impl Plugin for MenuPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, (menu_acquire_focus, menu_on_lose_focus).chain()) + .add_observer(menu_on_key_event) + .add_observer(menu_on_menu_event) + .add_observer(menu_item_on_pointer_down) + .add_observer(menu_item_on_pointer_up) + .add_observer(menu_item_on_pointer_click) + .add_observer(menu_item_on_pointer_drag_end) + .add_observer(menu_item_on_pointer_cancel); + } +} diff --git a/crates/bevy_ui_widgets/src/popover.rs b/crates/bevy_ui_widgets/src/popover.rs new file mode 100644 index 0000000000000..73591f715dca2 --- /dev/null +++ b/crates/bevy_ui_widgets/src/popover.rs @@ -0,0 +1,246 @@ +//! Framework for positioning of popups, tooltips, and other popover UI elements. + +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_camera::visibility::Visibility; +use bevy_ecs::{ + change_detection::DetectChangesMut, component::Component, hierarchy::ChildOf, query::Without, + schedule::IntoScheduleConfigs, system::Query, +}; +use bevy_math::{Rect, Vec2}; +use bevy_ui::{ + ComputedNode, ComputedUiRenderTargetInfo, Node, PositionType, UiGlobalTransform, UiSystems, Val, +}; + +/// Which side of the parent element the popover element should be placed. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub enum PopoverSide { + /// The popover element should be placed above the parent. + Top, + /// The popover element should be placed below the parent. + #[default] + Bottom, + /// The popover element should be placed to the left of the parent. + Left, + /// The popover element should be placed to the right of the parent. + Right, +} + +impl PopoverSide { + /// Returns the side that is the mirror image of this side. + pub fn mirror(&self) -> Self { + match self { + PopoverSide::Top => PopoverSide::Bottom, + PopoverSide::Bottom => PopoverSide::Top, + PopoverSide::Left => PopoverSide::Right, + PopoverSide::Right => PopoverSide::Left, + } + } +} + +/// How the popover element should be aligned to the parent element. The alignment will be along an +/// axis that is perpendicular to the direction of the popover side. So for example, if the popup is +/// positioned below the parent, then the [`PopoverAlign`] variant controls the horizontal aligment +/// of the popup. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub enum PopoverAlign { + /// The starting edge of the popover element should be aligned to the starting edge of the + /// parent. + #[default] + Start, + /// The ending edge of the popover element should be aligned to the ending edge of the parent. + End, + /// The center of the popover element should be aligned to the center of the parent. + Center, +} + +/// Indicates a possible position of a popover element relative to it's parent. You can +/// specify multiple possible positions; the positioning code will check to see if there is +/// sufficient space to display the popup without clipping. If any position has sufficient room, +/// it will pick the first one; if there are none, then it will pick the least bad one. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct PopoverPlacement { + /// The side of the parent entity where the popover element should be placed. + pub side: PopoverSide, + + /// How the popover element should be aligned to the parent entity. + pub align: PopoverAlign, + + /// The size of the gap between the parent and the popover element, in logical pixels. This will + /// offset the popover along the direction of [`side`]. + pub gap: f32, +} + +/// Component which is inserted into a popover element to make it dynamically position relative to +/// an parent element. +#[derive(Component, PartialEq, Default)] +pub struct Popover { + /// List of potential positions for the popover element relative to the parent. + pub positions: Vec, + + /// Indicates how close to the window edge the popup is allowed to go. + pub window_margin: f32, +} + +impl Clone for Popover { + fn clone(&self) -> Self { + Self { + positions: self.positions.clone(), + window_margin: self.window_margin, + } + } +} + +fn position_popover( + mut q_popover: Query<( + &mut Node, + &mut Visibility, + &ComputedNode, + &ComputedUiRenderTargetInfo, + &Popover, + &ChildOf, + )>, + q_parent: Query<(&ComputedNode, &UiGlobalTransform), Without>, +) { + for (mut node, mut visibility, computed_node, computed_target, popover, parent) in + q_popover.iter_mut() + { + // A rectangle which represents the area of the window. + let window_rect = Rect { + min: Vec2::ZERO, + max: computed_target.logical_size(), + } + .inflate(-popover.window_margin); + + // Compute the parent rectangle. + let Ok((parent_node, parent_transform)) = q_parent.get(parent.parent()) else { + continue; + }; + // Computed node size includes the border, but since absolute positioning doesn't include + // border we need to remove it from the calculations. + let parent_size = parent_node.size() + - Vec2::new( + parent_node.border.left + parent_node.border.right, + parent_node.border.top + parent_node.border.bottom, + ); + let parent_rect = Rect::from_center_size(parent_transform.translation, parent_size) + .scale(parent_node.inverse_scale_factor); + + let mut best_occluded = f32::MAX; + let mut best_rect = Rect::default(); + + // Loop through all the potential positions and find a good one. + for position in &popover.positions { + let popover_size = computed_node.size() * computed_node.inverse_scale_factor; + let mut rect = Rect::default(); + + let target_width = popover_size.x; + let target_height = popover_size.y; + + // Position along main axis. + match position.side { + PopoverSide::Top => { + rect.max.y = parent_rect.min.y - position.gap; + rect.min.y = rect.max.y - popover_size.y; + } + + PopoverSide::Bottom => { + rect.min.y = parent_rect.max.y + position.gap; + rect.max.y = rect.min.y + popover_size.y; + } + + PopoverSide::Left => { + rect.max.x = parent_rect.min.x - position.gap; + rect.min.x = rect.max.x - popover_size.x; + } + + PopoverSide::Right => { + rect.min.x = parent_rect.max.x + position.gap; + rect.max.x = rect.min.x + popover_size.x; + } + } + + // Position along secondary axis. + match position.align { + PopoverAlign::Start => match position.side { + PopoverSide::Top | PopoverSide::Bottom => { + rect.min.x = parent_rect.min.x; + rect.max.x = rect.min.x + target_width; + } + + PopoverSide::Left | PopoverSide::Right => { + rect.min.y = parent_rect.min.y; + rect.max.y = rect.min.y + target_height; + } + }, + + PopoverAlign::End => match position.side { + PopoverSide::Top | PopoverSide::Bottom => { + rect.max.x = parent_rect.max.x; + rect.min.x = rect.max.x - target_width; + } + + PopoverSide::Left | PopoverSide::Right => { + rect.max.y = parent_rect.max.y; + rect.min.y = rect.max.y - target_height; + } + }, + + PopoverAlign::Center => match position.side { + PopoverSide::Top | PopoverSide::Bottom => { + rect.min.x = (parent_rect.width() - target_width) * 0.5; + rect.max.x = rect.min.x + target_width; + } + + PopoverSide::Left | PopoverSide::Right => { + rect.min.y = (parent_rect.width() - target_height) * 0.5; + rect.max.y = rect.min.y + target_height; + } + }, + } + + // Clip to window and see how much of the popover element is occluded. We can calculate + // how much was clipped by intersecting the rectangle against the window bounds, and + // then subtracting the area from the area of the unclipped rectangle. + let clipped_rect = rect.intersect(window_rect); + let occlusion = rect.area() - clipped_rect.area(); + + // Find the position that has the least occlusion. + if occlusion < best_occluded { + best_occluded = occlusion; + best_rect = rect; + } + } + + // Update node properties, but only if they are different from before (to avoid setting + // change detection bit). + if best_occluded < f32::MAX { + let left = Val::Px(best_rect.min.x - parent_rect.min.x); + let top = Val::Px(best_rect.min.y - parent_rect.min.y); + visibility.set_if_neq(Visibility::Visible); + if node.left != left { + node.left = left; + } + if node.top != top { + node.top = top; + } + if node.bottom != Val::DEFAULT { + node.bottom = Val::DEFAULT; + } + if node.right != Val::DEFAULT { + node.right = Val::DEFAULT; + } + if node.position_type != PositionType::Absolute { + node.position_type = PositionType::Absolute; + } + } + } +} + +/// Plugin that adds systems for the [`Popover`] component. +pub struct PopoverPlugin; + +impl Plugin for PopoverPlugin { + fn build(&self, app: &mut App) { + app.add_systems(PostUpdate, position_popover.in_set(UiSystems::Prepare)); + } +} diff --git a/crates/bevy_ui_widgets/src/radio.rs b/crates/bevy_ui_widgets/src/radio.rs index 1dac539782f54..814cb0cd28a79 100644 --- a/crates/bevy_ui_widgets/src/radio.rs +++ b/crates/bevy_ui_widgets/src/radio.rs @@ -35,7 +35,7 @@ use crate::ValueChange; /// associated with a particular constant value, and would be checked whenever that value is equal /// to the group's value. This also means that as long as each button's associated value is unique /// within the group, it should never be the case that more than one button is selected at a time. -#[derive(Component, Debug)] +#[derive(Component, Debug, Clone, Default)] #[require(AccessibilityNode(accesskit::Node::new(Role::RadioGroup)))] pub struct RadioGroup; @@ -45,7 +45,7 @@ pub struct RadioGroup; /// According to the WAI-ARIA best practices document, radio buttons should not be focusable, /// but rather the enclosing group should be focusable. /// See / -#[derive(Component, Debug)] +#[derive(Component, Debug, Clone, Default)] #[require(AccessibilityNode(accesskit::Node::new(Role::RadioButton)), Checkable)] #[derive(Reflect)] #[reflect(Component)] diff --git a/crates/bevy_ui_widgets/src/slider.rs b/crates/bevy_ui_widgets/src/slider.rs index 59c3e580b19df..3fa57efe59a8b 100644 --- a/crates/bevy_ui_widgets/src/slider.rs +++ b/crates/bevy_ui_widgets/src/slider.rs @@ -72,7 +72,7 @@ pub enum TrackClick { /// /// In cases where overhang is desired for artistic reasons, the thumb may have additional /// decorative child elements, absolutely positioned, which don't affect the size measurement. -#[derive(Component, Debug, Default)] +#[derive(Component, Debug, Default, Clone)] #[require( AccessibilityNode(accesskit::Node::new(Role::Slider)), CoreSliderDragState, @@ -87,7 +87,7 @@ pub struct Slider { } /// Marker component that identifies which descendant element is the slider thumb. -#[derive(Component, Debug, Default)] +#[derive(Component, Debug, Default, Clone)] pub struct SliderThumb; /// A component which stores the current value of the slider. diff --git a/crates/bevy_window/src/cursor/custom_cursor.rs b/crates/bevy_window/src/cursor/custom_cursor.rs index 1b99e3f54ff9f..40c23250b01fb 100644 --- a/crates/bevy_window/src/cursor/custom_cursor.rs +++ b/crates/bevy_window/src/cursor/custom_cursor.rs @@ -8,7 +8,7 @@ use bevy_math::URect; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; /// A custom cursor created from an image. -#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -46,6 +46,19 @@ pub struct CustomCursorImage { pub hotspot: (u16, u16), } +impl Default for CustomCursorImage { + fn default() -> Self { + Self { + handle: Handle::default(), + texture_atlas: Default::default(), + flip_x: Default::default(), + flip_y: Default::default(), + rect: Default::default(), + hotspot: Default::default(), + } + } +} + /// A custom cursor created from a URL. Note that this currently only works on the web. #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] #[cfg_attr( diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index 5380fc393b875..269c457e6a280 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -26,9 +26,15 @@ enum AppState { Finished, } -#[derive(Resource, Default)] +#[derive(Resource)] struct RpgSpriteFolder(Handle); +impl Default for RpgSpriteFolder { + fn default() -> Self { + Self(Handle::default()) + } +} + fn load_textures(mut commands: Commands, asset_server: Res) { // Load multiple, individual sprites from a folder commands.insert_resource(RpgSpriteFolder(asset_server.load_folder("textures/rpg"))); diff --git a/examples/asset/asset_decompression.rs b/examples/asset/asset_decompression.rs index e514924ca9d0a..354a01f907d31 100644 --- a/examples/asset/asset_decompression.rs +++ b/examples/asset/asset_decompression.rs @@ -87,12 +87,21 @@ impl AssetLoader for GzAssetLoader { } } -#[derive(Component, Default)] +#[derive(Component)] struct Compressed { compressed: Handle, _phantom: PhantomData, } +impl Default for Compressed { + fn default() -> Self { + Self { + compressed: Handle::default(), + _phantom: Default::default(), + } + } +} + fn main() { App::new() .add_plugins(DefaultPlugins) diff --git a/examples/asset/custom_asset.rs b/examples/asset/custom_asset.rs index 8d4ac958ecdd1..ee7ce4349a1e1 100644 --- a/examples/asset/custom_asset.rs +++ b/examples/asset/custom_asset.rs @@ -102,7 +102,7 @@ fn main() { .run(); } -#[derive(Resource, Default)] +#[derive(Resource)] struct State { handle: Handle, other_handle: Handle, @@ -110,6 +110,17 @@ struct State { printed: bool, } +impl Default for State { + fn default() -> Self { + Self { + handle: Handle::default(), + other_handle: Handle::default(), + blob: Handle::default(), + printed: Default::default(), + } + } +} + fn setup(mut state: ResMut, asset_server: Res) { // Recommended way to load an asset state.handle = asset_server.load("data/asset.custom"); diff --git a/examples/games/alien_cake_addict.rs b/examples/games/alien_cake_addict.rs index 2e9f33bf739bb..938c44bda5601 100644 --- a/examples/games/alien_cake_addict.rs +++ b/examples/games/alien_cake_addict.rs @@ -58,7 +58,6 @@ struct Player { move_cooldown: Timer, } -#[derive(Default)] struct Bonus { entity: Option, i: usize, @@ -66,6 +65,17 @@ struct Bonus { handle: Handle, } +impl Default for Bonus { + fn default() -> Self { + Self { + entity: Default::default(), + i: Default::default(), + j: Default::default(), + handle: Handle::default(), + } + } +} + #[derive(Resource, Default)] struct Game { board: Vec>, diff --git a/examples/scene/bsn.rs b/examples/scene/bsn.rs new file mode 100644 index 0000000000000..9a2604c4f3a75 --- /dev/null +++ b/examples/scene/bsn.rs @@ -0,0 +1,199 @@ +//! This is a temporary stress test of various bsn! features. +// TODO: move these into actual tests and replace this with a more instructive user-facing example +use bevy::{ + prelude::*, + scene2::prelude::{Scene, *}, +}; +use bevy_scene2::SceneList; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, (print::, print::, print::)) + .run(); +} + +fn setup(mut commands: Commands, assets: Res) { + assets.load_scene("scene://base.bsn", base()); + assets.load_scene("scene://transform_1000.bsn", transform_1000()); + let top_level_handle = assets.load_scene("scene://top_level.bsn", top_level()); + commands.spawn(ScenePatchInstance(top_level_handle)); +} + +fn top_level() -> impl Scene { + let a = 20usize; + let b = 1993usize; + bsn! { + #TopLevel + :"scene://base.bsn" + :x + Sprite { size: b } + Team::Green(10) + {transform_1337()} + Children [ + Sprite { size: {4 + a}} + ] + } +} + +fn base() -> impl Scene { + let sprites = (0..10usize) + .map(|i| bsn! {Sprite { size: {i} }}) + .collect::>(); + + bsn! { + Name("Base") + Sprite { + handle: "asset://branding/bevy_bird_dark.png", + size: 1, + nested: Nested { + handle: @"asset://hello.png" + } + } + Transform::from_translation(Vec3::new(1.0, 1.0, 1.0)) + Team::Red { + x: 10, + y: Nested { + foo: 10 + }, + } + Gen:: { + value: 10, + } + on(|explode: On| { + }) + Foo(100, @"asset://branding/bevy_bird_dark.png") + [ + (:sprite_big Sprite { size: 2 }), + :widget(bsn_list![Text::new("hi")]), + {sprites}, + ] + } +} + +fn sprite_big() -> impl Scene { + bsn! { + Sprite { size: 100000, handle: "asset://branding/icon.png" } + } +} + +fn x() -> impl Scene { + bsn! { + :"scene://transform_1000.bsn" + Transform { translation: Vec3 { x: 11.0 } } + } +} + +fn transform_1000() -> impl Scene { + bsn! { + Transform { + translation: Vec3 { x: 1000.0, y: 1000.0 } + } + } +} + +fn transform_1337() -> impl Scene { + bsn! { + Transform { + translation: Vec3 { x: 1337.0 } + } + } +} + +#[derive(Component, Debug, GetTemplate)] +struct Sprite { + handle: Handle, + size: usize, + entity: Entity, + nested: Nested, +} + +#[derive(Component, Debug, GetTemplate)] +struct Gen>> { + size: usize, + value: T, +} + +#[derive(Clone, Debug, GetTemplate)] +struct Nested { + foo: usize, + #[template] + handle: Handle, +} + +#[derive(Component, Clone, Debug, GetTemplate)] +struct Foo(usize, #[template] Handle); + +#[derive(EntityEvent)] +struct Explode { + entity: Entity, +} + +#[derive(Component, Default, Clone)] +struct Thing; + +fn print( + query: Query<(Entity, Option<&Name>, Option<&ChildOf>, &C), Changed>, +) { + for (e, name, child_of, c) in &query { + println!("Changed {e:?} {name:?} {child_of:?} {c:#?}"); + } +} + +#[derive(Component, Debug, GetTemplate)] +enum Team { + Red { + x: usize, + y: Nested, + }, + Blue, + #[default] + Green(usize, usize), +} + +#[derive(Component, GetTemplate)] +enum Blah { + #[default] + A(Hi), + B(Arrrrg), + C(String), +} + +#[derive(Default)] +struct Arrrrg(Option>); + +impl Clone for Arrrrg { + fn clone(&self) -> Self { + Self(None) + } +} + +impl From for Arrrrg { + fn from(value: F) -> Self { + todo!() + } +} + +#[derive(Clone, Default)] +struct Hi { + size: usize, +} + +fn test() -> impl Scene { + bsn! { + Blah::A(Hi {size: 10}) + Blah::B(|world: &mut World| {}) + Blah::C("hi") + } +} + +fn widget(children: impl SceneList) -> impl Scene { + bsn! { + Node { + width: Val::Px(1.0) + } [ + {children} + ] + } +} diff --git a/examples/scene/ui_scene.rs b/examples/scene/ui_scene.rs new file mode 100644 index 0000000000000..6c8a0e40c6630 --- /dev/null +++ b/examples/scene/ui_scene.rs @@ -0,0 +1,67 @@ +#![allow(unused)] + +//! This example illustrates constructing ui scenes +use bevy::{ + ecs::template::template, + prelude::*, + scene2::prelude::{Scene, SpawnScene, *}, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .run(); +} + +fn setup(world: &mut World) { + world.spawn(Camera2d); + world.spawn_scene(ui()); +} + +fn ui() -> impl Scene { + bsn! { + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + } [ + :button("Button") + ] + } +} + +fn button(label: &'static str) -> impl Scene { + bsn! { + Button + Node { + width: Val::Px(150.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + } + BorderColor::from(Color::BLACK) + BorderRadius::MAX + BackgroundColor(Color::srgb(0.15, 0.15, 0.15)) + on(|event: On>| { + println!("pressed"); + }) + [( + Text(label) + // The `template` wrapper can be used for types that can't implement or don't yet have a template + template(|context| { + Ok(TextFont { + font: context + .resource::() + .load("fonts/FiraSans-Bold.ttf"), + font_size: 33.0, + ..default() + }) + }) + TextColor(Color::srgb(0.9, 0.9, 0.9)) + TextShadow + )] + } +} diff --git a/examples/ui/feathers.rs b/examples/ui/feathers.rs index 13ea3c212d810..542d127d669e7 100644 --- a/examples/ui/feathers.rs +++ b/examples/ui/feathers.rs @@ -4,9 +4,9 @@ use bevy::{ color::palettes, feathers::{ controls::{ - button, checkbox, color_slider, color_swatch, radio, slider, toggle_switch, - ButtonProps, ButtonVariant, ColorChannel, ColorSlider, ColorSliderProps, ColorSwatch, - SliderBaseColor, SliderProps, + button, checkbox, color_slider, color_swatch, menu, menu_button, menu_item, menu_popup, + radio, slider, toggle_switch, ButtonProps, ButtonVariant, ColorChannel, ColorSlider, + ColorSliderProps, ColorSwatch, MenuButtonProps, SliderBaseColor, SliderProps, }, dark_theme::create_dark_theme, rounded_corners::RoundedCorners, @@ -15,9 +15,10 @@ use bevy::{ }, input_focus::tab_navigation::TabGroup, prelude::*, + scene2::prelude::{Scene, *}, ui::{Checked, InteractionDisabled}, ui_widgets::{ - checkbox_self_update, observe, slider_self_update, Activate, RadioButton, RadioGroup, + checkbox_self_update, slider_self_update, Activate, RadioButton, RadioGroup, SliderPrecision, SliderStep, SliderValue, ValueChange, }, }; @@ -29,13 +30,14 @@ struct DemoWidgetStates { hsl_color: Hsla, } -#[derive(Component, Clone, Copy, PartialEq)] +#[derive(Component, Clone, Copy, PartialEq, GetTemplate)] enum SwatchType { + #[default] Rgb, Hsl, } -#[derive(Component, Clone, Copy)] +#[derive(Component, Clone, Copy, Default)] struct DemoDisabledButton; fn main() { @@ -54,11 +56,11 @@ fn main() { fn setup(mut commands: Commands) { // ui camera commands.spawn(Camera2d); - commands.spawn(demo_root()); + commands.spawn_scene(demo_root()); } -fn demo_root() -> impl Bundle { - ( +fn demo_root() -> impl Scene { + bsn! { Node { width: percent(100), height: percent(100), @@ -67,11 +69,10 @@ fn demo_root() -> impl Bundle { display: Display::Flex, flex_direction: FlexDirection::Column, row_gap: px(10), - ..default() - }, - TabGroup::default(), - ThemeBackgroundColor(tokens::WINDOW_BG), - children![( + } + TabGroup + ThemeBackgroundColor(tokens::WINDOW_BG) + [( Node { display: Display::Flex, flex_direction: FlexDirection::Column, @@ -81,9 +82,8 @@ fn demo_root() -> impl Bundle { row_gap: px(8), width: percent(30), min_width: px(200), - ..default() - }, - children![ + } + [ ( Node { display: Display::Flex, @@ -91,42 +91,72 @@ fn demo_root() -> impl Bundle { align_items: AlignItems::Center, justify_content: JustifyContent::Start, column_gap: px(8), - ..default() - }, - children![ + } + [ ( - button( - ButtonProps::default(), - (), - Spawn((Text::new("Normal"), ThemedText)) - ), - observe(|_activate: On| { + button(ButtonProps::default()) + on(|_: On| { info!("Normal button clicked!"); }) + [ (Text::new("Normal") ThemedText) ] ), ( button( ButtonProps::default(), - (InteractionDisabled, DemoDisabledButton), - Spawn((Text::new("Disabled"), ThemedText)) - ), - observe(|_activate: On| { + ) + InteractionDisabled + DemoDisabledButton + on(|_: On| { info!("Disabled button clicked!"); }) + [ (Text::new("Disabled") ThemedText) ] ), ( button( ButtonProps { variant: ButtonVariant::Primary, ..default() - }, - (), - Spawn((Text::new("Primary"), ThemedText)) - ), - observe(|_activate: On| { - info!("Disabled button clicked!"); + } + ) + on(|_: On| { + info!("Primary button clicked!"); }) + [ (Text::new("Primary") ThemedText) ] ), + ( + menu(|parent| { + parent.spawn_related_scenes::(bsn_list!( + :menu_popup() + [ + ( + :menu_item() + on(|_: On| { + info!("Menu button clicked!"); + }) + [ + (Text("MenuItem") ThemedText) + ] + ), + ( + :menu_item() + on(|_: On| { + info!("Menu button clicked!"); + }) + [ + (Text("MenuItem") ThemedText) + ] + ) + ] + )); + }) [ + ( + :menu_button(MenuButtonProps::default()) + [ + (Text("Menu") ThemedText) + ] + ) + ] + ) ] ), ( @@ -136,21 +166,19 @@ fn demo_root() -> impl Bundle { align_items: AlignItems::Center, justify_content: JustifyContent::Start, column_gap: px(1), - ..default() - }, - children![ + } + [ ( button( ButtonProps { corners: RoundedCorners::Left, ..default() }, - (), - Spawn((Text::new("Left"), ThemedText)) - ), - observe(|_activate: On| { + ) + on(|_: On| { info!("Left button clicked!"); }) + [ (Text::new("Left") ThemedText) ] ), ( button( @@ -158,12 +186,11 @@ fn demo_root() -> impl Bundle { corners: RoundedCorners::None, ..default() }, - (), - Spawn((Text::new("Center"), ThemedText)) - ), - observe(|_activate: On| { + ) + on(|_: On| { info!("Center button clicked!"); }) + [ (Text::new("Center") ThemedText) ] ), ( button( @@ -171,28 +198,27 @@ fn demo_root() -> impl Bundle { variant: ButtonVariant::Primary, corners: RoundedCorners::Right, }, - (), - Spawn((Text::new("Right"), ThemedText)) - ), - observe(|_activate: On| { + ) + on(|_: On| { info!("Right button clicked!"); }) + [ (Text::new("Right") ThemedText) ] ), ] ), ( button( ButtonProps::default(), - (), - Spawn((Text::new("Button"), ThemedText)) - ), - observe(|_activate: On| { + ) + on(|_: On| { info!("Wide button clicked!"); }) + [ (Text::new("Button") ThemedText) ] ), ( - checkbox(Checked, Spawn((Text::new("Checkbox"), ThemedText))), - observe( + checkbox() + Checked + on( |change: On>, query: Query>, mut commands: Commands| { @@ -211,34 +237,33 @@ fn demo_root() -> impl Bundle { } } ) + [ (Text::new("Checkbox") ThemedText) ] ), ( - checkbox( - InteractionDisabled, - Spawn((Text::new("Disabled"), ThemedText)) - ), - observe(|_change: On>| { + checkbox() + InteractionDisabled + on(|_change: On>| { warn!("Disabled checkbox clicked!"); }) + [ (Text::new("Disabled") ThemedText) ] ), ( - checkbox( - (InteractionDisabled, Checked), - Spawn((Text::new("Disabled+Checked"), ThemedText)) - ), - observe(|_change: On>| { + checkbox() + InteractionDisabled + Checked + on(|_change: On>| { warn!("Disabled checkbox clicked!"); }) + [ (Text::new("Disabled+Checked") ThemedText) ] ), ( Node { display: Display::Flex, flex_direction: FlexDirection::Column, row_gap: px(4), - ..default() - }, - RadioGroup, - observe( + } + RadioGroup + on( |value_change: On>, q_radio: Query>, mut commands: Commands| { @@ -250,15 +275,12 @@ fn demo_root() -> impl Bundle { } } } - ), - children![ - radio(Checked, Spawn((Text::new("One"), ThemedText))), - radio((), Spawn((Text::new("Two"), ThemedText))), - radio((), Spawn((Text::new("Three"), ThemedText))), - radio( - InteractionDisabled, - Spawn((Text::new("Disabled"), ThemedText)) - ), + ) + [ + radio() Checked Children [ (Text::new("One") ThemedText) ], + radio() [ (Text::new("Two") ThemedText) ], + radio() [ (Text::new("Three") ThemedText) ], + radio() InteractionDisabled Children [ (Text::new("Disabled") ThemedText) ] ] ), ( @@ -268,159 +290,126 @@ fn demo_root() -> impl Bundle { align_items: AlignItems::Center, justify_content: JustifyContent::Start, column_gap: px(8), - ..default() - }, - children![ - (toggle_switch((),), observe(checkbox_self_update)), + } + [ + (toggle_switch() on(checkbox_self_update)), ( - toggle_switch(InteractionDisabled,), - observe(checkbox_self_update) + toggle_switch() + InteractionDisabled + on(checkbox_self_update) ), ( - toggle_switch((InteractionDisabled, Checked),), - observe(checkbox_self_update) + toggle_switch() + InteractionDisabled + Checked + on(checkbox_self_update) ), ] ), ( - slider( - SliderProps { - max: 100.0, - value: 20.0, - ..default() - }, - (SliderStep(10.), SliderPrecision(2)), - ), - observe(slider_self_update) + slider(SliderProps { + max: 100.0, + value: 20.0, + ..default() + }) + SliderStep(10.) + SliderPrecision(2) + on(slider_self_update) ), ( Node { display: Display::Flex, flex_direction: FlexDirection::Row, justify_content: JustifyContent::SpaceBetween, - ..default() - }, - children![Text("Srgba".to_owned()), color_swatch(SwatchType::Rgb),] + } + [Text("Srgba"), (color_swatch() SwatchType::Rgb)] ), ( - color_slider( - ColorSliderProps { - value: 0.5, - channel: ColorChannel::Red - }, - () - ), - observe( - |change: On>, mut color: ResMut| { + color_slider(ColorSliderProps { + value: 0.5, + channel: ColorChannel::Red + }) + on(|change: On>, mut color: ResMut| { color.rgb_color.red = change.value; - } - ) + }) ), ( - color_slider( - ColorSliderProps { - value: 0.5, - channel: ColorChannel::Green - }, - () - ), - observe( - |change: On>, mut color: ResMut| { - color.rgb_color.green = change.value; - }, - ) + color_slider(ColorSliderProps { + value: 0.5, + channel: ColorChannel::Green + }) + on(|change: On>, mut color: ResMut| { + color.rgb_color.green = change.value; + }) ), ( - color_slider( - ColorSliderProps { - value: 0.5, - channel: ColorChannel::Blue - }, - () - ), - observe( - |change: On>, mut color: ResMut| { - color.rgb_color.blue = change.value; - }, - ) + color_slider(ColorSliderProps { + value: 0.5, + channel: ColorChannel::Blue + }) + on(|change: On>, mut color: ResMut| { + color.rgb_color.blue = change.value; + }) ), ( - color_slider( - ColorSliderProps { - value: 0.5, - channel: ColorChannel::Alpha - }, - () - ), - observe( - |change: On>, mut color: ResMut| { - color.rgb_color.alpha = change.value; - }, - ) + color_slider(ColorSliderProps { + value: 0.5, + channel: ColorChannel::Alpha + }) + on(|change: On>, mut color: ResMut| { + color.rgb_color.alpha = change.value; + }) ), ( Node { display: Display::Flex, flex_direction: FlexDirection::Row, justify_content: JustifyContent::SpaceBetween, - ..default() - }, - children![Text("Hsl".to_owned()), color_swatch(SwatchType::Hsl),] + } + [Text("Hsl"), (color_swatch() SwatchType::Hsl)] ), ( - color_slider( - ColorSliderProps { - value: 0.5, - channel: ColorChannel::HslHue - }, - () - ), - observe( - |change: On>, mut color: ResMut| { - color.hsl_color.hue = change.value; - }, - ) + color_slider(ColorSliderProps { + value: 0.5, + channel: ColorChannel::HslHue + }) + on(|change: On>, mut color: ResMut| { + color.hsl_color.hue = change.value; + }) ), ( - color_slider( - ColorSliderProps { - value: 0.5, - channel: ColorChannel::HslSaturation - }, - () - ), - observe( - |change: On>, mut color: ResMut| { - color.hsl_color.saturation = change.value; - }, - ) + color_slider(ColorSliderProps { + value: 0.5, + channel: ColorChannel::HslSaturation + }) + on(|change: On>, mut color: ResMut| { + color.hsl_color.saturation = change.value; + }) ), ( - color_slider( - ColorSliderProps { - value: 0.5, - channel: ColorChannel::HslLightness - }, - () - ), - observe( - |change: On>, mut color: ResMut| { - color.hsl_color.lightness = change.value; - }, - ) + color_slider(ColorSliderProps { + value: 0.5, + channel: ColorChannel::HslLightness + }) + on(|change: On>, mut color: ResMut| { + color.hsl_color.lightness = change.value; + }) ) ] - ),], - ) + )] + } } fn update_colors( colors: Res, - mut sliders: Query<(Entity, &ColorSlider, &mut SliderBaseColor)>, - swatches: Query<(&SwatchType, &Children), With>, + mut sliders: Query<(Entity, Ref, &mut SliderBaseColor)>, + swatches: Query<(Ref, &Children), With>, mut commands: Commands, ) { - if colors.is_changed() { + // Check to see if sliders or swatches were added after resource has changed. + let sliders_added = sliders.iter().any(|(_, slider, _)| slider.is_added()); + let swatches_added = swatches.iter().any(|(swatch, _)| swatch.is_added()); + if colors.is_changed() || sliders_added || swatches_added { for (slider_ent, slider, mut base) in sliders.iter_mut() { match slider.channel { ColorChannel::Red => { @@ -471,7 +460,7 @@ fn update_colors( for (swatch_type, children) in swatches.iter() { commands .entity(children[0]) - .insert(BackgroundColor(match swatch_type { + .insert(BackgroundColor(match *swatch_type { SwatchType::Rgb => colors.rgb_color.into(), SwatchType::Hsl => colors.hsl_color.into(), })); diff --git a/examples/ui/virtual_keyboard.rs b/examples/ui/virtual_keyboard.rs index e423adc1737bc..6bc3a84c297b1 100644 --- a/examples/ui/virtual_keyboard.rs +++ b/examples/ui/virtual_keyboard.rs @@ -9,7 +9,7 @@ use bevy::{ FeathersPlugins, }, prelude::*, - ui_widgets::observe, + scene2::prelude::*, }; fn main() { @@ -37,34 +37,32 @@ fn setup(mut commands: Commands) { vec!["left", "right", "up", "down", "home", "end"], ]; - commands.spawn(( + commands.spawn_scene(bsn! { Node { width: percent(100), height: percent(100), align_items: AlignItems::End, justify_content: JustifyContent::Center, - ..default() - }, - children![( + } + [( Node { flex_direction: FlexDirection::Column, - border: px(5).into(), + border: px(5), row_gap: px(5), - padding: px(5).into(), + padding: px(5), align_items: AlignItems::Center, - margin: px(25).into(), - ..Default::default() - }, - BackgroundColor(NAVY.into()), - BorderColor::all(Color::WHITE), - BorderRadius::all(px(10)), - children![ + margin: px(25), + } + BackgroundColor(NAVY) + BorderColor::all(Color::WHITE) + BorderRadius::all(px(10)) + [ Text::new("virtual keyboard"), ( - virtual_keyboard(layout.into_iter()), - observe(on_virtual_key_pressed) + virtual_keyboard(layout.into_iter()) + on(on_virtual_key_pressed) ) ] - )], - )); + )] + }); }