diff --git a/crates/bevy_ecs/macros/src/template.rs b/crates/bevy_ecs/macros/src/template.rs index 6fbf6e4781001..484b5bae82929 100644 --- a/crates/bevy_ecs/macros/src/template.rs +++ b/crates/bevy_ecs/macros/src/template.rs @@ -2,12 +2,13 @@ use bevy_macro_utils::BevyManifest; use proc_macro::TokenStream; use quote::{format_ident, quote}; use syn::{ - parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, Data, DeriveInput, - Fields, FieldsUnnamed, Index, Path, Result, Token, WhereClause, + parse::ParseStream, parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, + Data, DeriveInput, Fields, FieldsUnnamed, Ident, Index, Path, Result, Token, WhereClause, }; const TEMPLATE_DEFAULT_ATTRIBUTE: &str = "default"; const TEMPLATE_ATTRIBUTE: &str = "template"; +const BUILT_IN_ATTRIBUTE: &str = "built_in"; pub(crate) fn derive_from_template(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); @@ -332,6 +333,12 @@ struct StructImpl { template_field_clones: Vec, } +enum TemplateType { + FromTemplate, + BuiltIn, + Manual(Path), +} + fn struct_impl(fields: &Fields, bevy_ecs: &Path, is_enum: bool) -> Result { let mut template_fields = Vec::with_capacity(fields.len()); let mut template_field_builds = Vec::with_capacity(fields.len()); @@ -344,22 +351,39 @@ fn struct_impl(fields: &Fields, bevy_ecs: &Path, is_enum: bool) -> Result() else { - return Err(syn::Error::new( - attr.span(), - "Expected a Template type path", - )); - }; - template_type = Some(quote!(#path)); + attr.parse_args_with(|stream: ParseStream| { + let forked = stream.fork(); + let ident = forked.parse::()?; + if ident == BUILT_IN_ATTRIBUTE { + stream.parse::()?; + template_type = TemplateType::BuiltIn; + } else { + if let Ok(path) = stream.parse::() { + template_type = TemplateType::Manual(path); + } else { + return Err(syn::Error::new( + attr.span(), + "Expected a Template type path", + )); + } + } + Ok(()) + })?; } } - if template_type.is_none() { - template_type = Some(quote!(<#ty as #bevy_ecs::template::FromTemplate>::Template)); - } + let template_type = match template_type { + TemplateType::FromTemplate => { + quote!(<#ty as #bevy_ecs::template::FromTemplate>::Template) + } + TemplateType::BuiltIn => { + quote!(<#ty as #bevy_ecs::template::BuiltInTemplate>::Template) + } + TemplateType::Manual(path) => quote! {#path}, + }; if is_named { template_fields.push(quote! { diff --git a/crates/bevy_ecs/src/template.rs b/crates/bevy_ecs/src/template.rs index 31a6d77472d50..5fef59fc7d3fe 100644 --- a/crates/bevy_ecs/src/template.rs +++ b/crates/bevy_ecs/src/template.rs @@ -267,6 +267,72 @@ impl ScopedEntities { /// } /// } /// ``` +/// +/// [`FromTemplate`] is automatically implemented for anything that is [`Default`] and [`Clone`]. "Built in" collection types like +/// [`Option`] and [`Vec`] pick up this "blanket" implementation, which is generally a good thing because it means these collection +/// types work with [`FromTemplate`] derives by default. However if the items in the collection have a custom [`FromTemplate`] impl +/// (ex: a manual implementation like `Handle` for assets or an explicit [`FromTemplate`] derive), then relying on a [`Default`] / +/// [`Clone`] implementation doesn't work, as that won't run the template logic! +/// +/// Therefore, cases like [`Option>`] need something other than [`FromTemplate`] to determine the type. One option is to specify +/// the template manually: +/// +/// ``` +/// # use bevy_ecs::{prelude::*, template::{TemplateContext, OptionTemplate}}; +/// # use core::marker::PhantomData; +/// # struct Handle(PhantomData); +/// # struct HandleTemplate(PhantomData); +/// # struct Image; +/// # impl FromTemplate for Handle { +/// # type Template = HandleTemplate; +/// # } +/// # impl Template for HandleTemplate { +/// # type Output = Handle; +/// # fn build_template(&self, context: &mut TemplateContext) -> Result { +/// # unimplemented!() +/// # } +/// # fn clone_template(&self) -> Self { +/// # unimplemented!() +/// # } +/// # } +/// #[derive(FromTemplate)] +/// struct Widget { +/// #[template(OptionTemplate>)] +/// image: Option> +/// } +/// ``` +/// +/// However that is a bit of a mouthfull! This is where [`BuiltInTemplate`] comes in. It fills the same role +/// as [`FromTemplate`], but has no blanket implementation for [`Default`] and [`Clone`], meaning we can have +/// custom implementations for types like [`Option`] and [`Vec`]. +/// +/// If you deriving [`FromTemplate`] and you have a "built in" type like [`Option>`] which has custom template logic, +/// annotate it with the `template(built_in)` attribute to use [`BuiltInTemplate`] instead of [`FromTemplate`]: +/// +/// ``` +/// # use bevy_ecs::{prelude::*, template::TemplateContext}; +/// # use core::marker::PhantomData; +/// # struct Handle(PhantomData); +/// # struct HandleTemplate(PhantomData); +/// # struct Image; +/// # impl FromTemplate for Handle { +/// # type Template = HandleTemplate; +/// # } +/// # impl Template for HandleTemplate { +/// # type Output = Handle; +/// # fn build_template(&self, context: &mut TemplateContext) -> Result { +/// # unimplemented!() +/// # } +/// # fn clone_template(&self) -> Self { +/// # unimplemented!() +/// # } +/// # } +/// #[derive(FromTemplate)] +/// struct Widget { +/// #[template(built_in)] +/// image: Option> +/// } +/// ``` pub trait FromTemplate: Sized { /// The [`Template`] for this type. type Template: Template; @@ -431,12 +497,49 @@ pub fn template Result, O>(func: F) -> FnTempl FnTemplate(func) } -/// A [`Template`] for Option. -pub struct OptionTemplate(Option); +/// Roughly equivalent to [`FromTemplate`], but does not have a blanket implementation for [`Default`] + [`Clone`] types. +/// This is generally used for common generic collection types like [`Option`] and [`Vec`], which have [`Default`] + [`Clone`] impls and +/// therefore also pick up the [`FromTemplate`] behavior. This is fine when the `T` in [`Option`] is not "templated" +/// (ex: does not have an explicit [`FromTemplate`] derive). But if `T` is "templated", such as [`Option>`], then it would require +/// a manual `#[template(OptionTemplate>)]` field annotation. This isn't fun to type out. +/// +/// [`BuiltInTemplate`] enables equivalent "template type inference", by annotating a field with a type that implements [`BuiltInTemplate`] with +/// `#[template(built_in)]`. +pub trait BuiltInTemplate: Sized { + /// The template to consider the "built in" template for this type. + type Template: Template; +} -impl Default for OptionTemplate { - fn default() -> Self { - Self(None) +impl BuiltInTemplate for Option { + type Template = OptionTemplate; +} + +impl BuiltInTemplate for Vec { + type Template = VecTemplate; +} + +/// A [`Template`] for [`Option`]. +#[derive(Default)] +pub enum OptionTemplate { + /// Template of [`Option::Some`]. + Some(T), + /// Template of [`Option::None`]. + #[default] + None, +} + +impl From> for OptionTemplate { + fn from(value: Option) -> Self { + match value { + Some(value) => OptionTemplate::Some(value), + None => OptionTemplate::None, + } + } +} + +impl From for OptionTemplate { + fn from(value: T) -> Self { + OptionTemplate::Some(value) } } @@ -444,13 +547,66 @@ impl Template for OptionTemplate { type Output = Option; fn build_template(&self, context: &mut TemplateContext) -> Result { - Ok(match &self.0 { - Some(template) => Some(template.build_template(context)?), - None => None, + Ok(match &self { + OptionTemplate::Some(template) => Some(template.build_template(context)?), + OptionTemplate::None => None, }) } fn clone_template(&self) -> Self { - OptionTemplate(self.0.as_ref().map(Template::clone_template)) + match self { + OptionTemplate::Some(value) => OptionTemplate::Some(value.clone_template()), + OptionTemplate::None => OptionTemplate::None, + } + } +} + +/// A [`Template`] for [`Vec`]. +pub struct VecTemplate(pub Vec); + +impl Default for VecTemplate { + fn default() -> Self { + Self(Vec::new()) + } +} + +impl Template for VecTemplate { + type Output = Vec; + + fn build_template(&self, context: &mut TemplateContext) -> Result { + let mut output = Vec::with_capacity(self.0.len()); + for value in &self.0 { + output.push(value.build_template(context)?); + } + Ok(output) + } + + fn clone_template(&self) -> Self { + VecTemplate(self.0.iter().map(Template::clone_template).collect()) + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + use alloc::string::{String, ToString}; + + #[test] + fn option_template() { + #[derive(FromTemplate)] + struct Handle(String); + + #[derive(FromTemplate)] + struct Foo { + #[template(built_in)] + handle: Option, + } + + let mut world = World::new(); + let foo_template = FooTemplate { + handle: Some(HandleTemplate("handle_path".to_string())).into(), + }; + let foo = world.spawn_empty().build_template(&foo_template).unwrap(); + assert_eq!(foo.handle.unwrap().0, "handle_path".to_string()); } } diff --git a/crates/bevy_ecs/src/world/entity_access/world_mut.rs b/crates/bevy_ecs/src/world/entity_access/world_mut.rs index c82590159cdb2..9b3471f4ff541 100644 --- a/crates/bevy_ecs/src/world/entity_access/world_mut.rs +++ b/crates/bevy_ecs/src/world/entity_access/world_mut.rs @@ -16,6 +16,7 @@ use crate::{ relationship::RelationshipHookMode, resource::Resource, storage::{SparseSets, Table}, + template::{EntityScopes, ScopedEntities, Template, TemplateContext}, world::{ error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, ComponentEntry, DynamicComponentFetch, EntityMut, EntityRef, FilteredEntityMut, FilteredEntityRef, Mut, @@ -1594,6 +1595,22 @@ impl<'w> EntityWorldMut<'w> { self.entity } + /// Creates a new [`TemplateContext`] for this entity and passes it into the given `func`. + pub fn template_context( + &mut self, + func: impl FnOnce(&mut TemplateContext) -> crate::error::Result, + ) -> crate::error::Result { + let mut scoped_entities = ScopedEntities::new(0); + let entity_scopes = EntityScopes::default(); + let mut context = TemplateContext::new(self, &mut scoped_entities, &entity_scopes); + func(&mut context) + } + + /// Builds the given template using a [`TemplateContext`] generated for this entity. + pub fn build_template(&mut self, template: &T) -> crate::error::Result { + self.template_context(|context| template.build_template(context)) + } + /// This despawns this entity if it is currently spawned, storing the new [`EntityGeneration`](crate::entity::EntityGeneration) in [`Self::entity`] but not freeing it. pub(crate) fn despawn_no_free_with_caller(&mut self, caller: MaybeLocation) { // setup diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index efb20f2c508f0..534c99e2f0423 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -2,12 +2,8 @@ use bevy_asset::{AsAssetId, AssetId, Assets, Handle}; use bevy_camera::visibility::{self, Visibility, VisibilityClass}; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{ - component::Component, - reflect::ReflectComponent, - template::{FromTemplate, OptionTemplate}, -}; -use bevy_image::{Image, TextureAtlas, TextureAtlasLayout, TextureAtlasTemplate}; +use bevy_ecs::{component::Component, reflect::ReflectComponent, template::FromTemplate}; +use bevy_image::{Image, TextureAtlas, TextureAtlasLayout}; use bevy_math::{Rect, UVec2, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, PartialReflect, Reflect}; use bevy_transform::components::Transform; @@ -24,7 +20,7 @@ pub struct Sprite { /// The image used to render the sprite pub image: Handle, /// The (optional) texture atlas used to render the sprite - #[template(OptionTemplate)] + #[template(built_in)] pub texture_atlas: Option, /// The sprite's color tint pub color: Color, diff --git a/crates/bevy_sprite/src/sprite_mesh.rs b/crates/bevy_sprite/src/sprite_mesh.rs index e9667fa29adcf..5462f3e7d5a12 100644 --- a/crates/bevy_sprite/src/sprite_mesh.rs +++ b/crates/bevy_sprite/src/sprite_mesh.rs @@ -1,12 +1,8 @@ use bevy_asset::{Assets, Handle}; use bevy_camera::visibility::{Visibility, VisibilityClass}; use bevy_color::Color; -use bevy_ecs::{ - component::Component, - reflect::ReflectComponent, - template::{FromTemplate, OptionTemplate}, -}; -use bevy_image::{Image, TextureAtlas, TextureAtlasLayout, TextureAtlasTemplate}; +use bevy_ecs::{component::Component, reflect::ReflectComponent, template::FromTemplate}; +use bevy_image::{Image, TextureAtlas, TextureAtlasLayout}; use bevy_math::{Rect, UVec2, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, PartialReflect, Reflect}; use bevy_transform::components::Transform; @@ -24,7 +20,7 @@ pub struct SpriteMesh { /// The image used to render the sprite pub image: Handle, /// The (optional) texture atlas used to render the sprite - #[template(OptionTemplate)] + #[template(built_in)] pub texture_atlas: Option, /// The sprite's color tint pub color: Color, diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index d4cf8523edd10..fa3fe0a12f3a4 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -1,8 +1,8 @@ use crate::{ComputedUiRenderTargetInfo, ContentSize, Measure, MeasureArgs, Node, NodeMeasure}; use bevy_asset::{AsAssetId, AssetId, Assets, Handle}; use bevy_color::Color; -use bevy_ecs::{prelude::*, template::OptionTemplate}; -use bevy_image::{prelude::*, TextureAtlasTemplate, TRANSPARENT_IMAGE_HANDLE}; +use bevy_ecs::prelude::*; +use bevy_image::{prelude::*, TRANSPARENT_IMAGE_HANDLE}; use bevy_math::{Rect, UVec2, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_sprite::TextureSlicer; @@ -23,7 +23,7 @@ pub struct ImageNode { /// This defaults to a [`TRANSPARENT_IMAGE_HANDLE`], which points to a fully transparent 1x1 texture. pub image: Handle, /// The (optional) texture atlas used to render the image. - #[template(OptionTemplate)] + #[template(built_in)] pub texture_atlas: Option, /// Whether the image should be flipped along its x-axis. pub flip_x: bool, diff --git a/crates/bevy_window/src/cursor/custom_cursor.rs b/crates/bevy_window/src/cursor/custom_cursor.rs index 3654e33f663ee..ddd173e6cf080 100644 --- a/crates/bevy_window/src/cursor/custom_cursor.rs +++ b/crates/bevy_window/src/cursor/custom_cursor.rs @@ -1,8 +1,8 @@ use crate::cursor::CursorIcon; use alloc::string::String; use bevy_asset::Handle; -use bevy_ecs::template::{FromTemplate, OptionTemplate}; -use bevy_image::{Image, TextureAtlas, TextureAtlasTemplate}; +use bevy_ecs::template::FromTemplate; +use bevy_image::{Image, TextureAtlas}; use bevy_math::URect; #[cfg(feature = "bevy_reflect")] @@ -20,7 +20,7 @@ pub struct CustomCursorImage { /// or 32 bit float rgba. PNG images work well for this. pub handle: Handle, /// An optional texture atlas used to render the image. - #[template(OptionTemplate)] + #[template(built_in)] pub texture_atlas: Option, /// Whether the image should be flipped along its x-axis. ///