diff --git a/Cargo.toml b/Cargo.toml index f047040bdc9f7..971b4b7f7567e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,6 +145,7 @@ default = [ "bevy_picking", "bevy_render", "bevy_scene", + "bevy_scene2", "bevy_sprite", "bevy_sprite_picking_backend", "bevy_state", @@ -167,6 +168,7 @@ default = [ "x11", "debug", "zstd_rust", + "experimental_bevy_feathers", ] # Recommended defaults for no_std applications @@ -249,6 +251,9 @@ bevy_render = ["bevy_internal/bevy_render", "bevy_color"] # Provides scene functionality bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"] +# Provides scene functionality +bevy_scene2 = ["bevy_internal/bevy_scene2", "bevy_asset"] + # Provides raytraced lighting (experimental) bevy_solari = [ "bevy_internal/bevy_solari", @@ -598,8 +603,9 @@ flate2 = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1.0.140" bytemuck = "1" -bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false } # The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself. +bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false } +bevy_scene2 = { path = "crates/bevy_scene2", version = "0.17.0-dev", default-features = false } bevy_ecs = { path = "crates/bevy_ecs", version = "0.17.0-dev", default-features = false } bevy_state = { path = "crates/bevy_state", version = "0.17.0-dev", default-features = false } bevy_asset = { path = "crates/bevy_asset", version = "0.17.0-dev", default-features = false } @@ -619,6 +625,7 @@ anyhow = "1" macro_rules_attribute = "0.2" accesskit = "0.19" nonmax = "0.5" +variadics_please = "1" [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] smol = "2" @@ -2788,6 +2795,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 22b2f71f075e9..bcf7ae70e972d 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -120,7 +120,7 @@ impl ManageAccessibilityUpdates { /// /// If the entity doesn't have a parent, or if the immediate parent doesn't have /// an `AccessibilityNode`, its node will be an immediate child of the primary window. -#[derive(Component, Clone, Deref, DerefMut)] +#[derive(Component, Clone, Deref, DerefMut, Default)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct AccessibilityNode(pub Node); diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index adb4a7c7ac541..8c8402115b37b 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 838c618d8ed1b..12ed0be85341e 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}, + world::EntityWorldMut, +}; +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, entity: &mut EntityWorldMut) -> Result> { + Ok(entity.resource::().load(&self.path)) } } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 8186b6315d5b4..d3c51ec9eb8f8 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -472,6 +472,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()); @@ -486,18 +492,18 @@ impl VisitAssetDependencies for Option { } } -impl VisitAssetDependencies for Vec> { +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 Vec { +impl VisitAssetDependencies for HashSet, S> { fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { for dependency in self { - visit(dependency.id()); + visit(dependency.id().untyped()); } } } diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 69dc8428da87d..da3d59899955f 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -864,6 +864,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_core_pipeline/src/auto_exposure/settings.rs b/crates/bevy_core_pipeline/src/auto_exposure/settings.rs index ae359a8a01dd4..ecee10553b239 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/settings.rs +++ b/crates/bevy_core_pipeline/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_core_widgets/src/callback.rs b/crates/bevy_core_widgets/src/callback.rs index 37905e221cfcf..c3081837a785d 100644 --- a/crates/bevy_core_widgets/src/callback.rs +++ b/crates/bevy_core_widgets/src/callback.rs @@ -1,5 +1,7 @@ -use bevy_ecs::system::{Commands, SystemId, SystemInput}; +use bevy_ecs::system::{Commands, IntoSystem, SystemId, SystemInput}; +use bevy_ecs::template::{GetTemplate, Template}; use bevy_ecs::world::{DeferredWorld, World}; +use std::marker::PhantomData; /// A callback defines how we want to be notified when a widget changes state. Unlike an event /// or observer, callbacks are intended for "point-to-point" communication that cuts across the @@ -27,15 +29,119 @@ use bevy_ecs::world::{DeferredWorld, World}; /// // Later, when we want to execute the callback: /// app.world_mut().commands().notify(&callback); /// ``` -#[derive(Default, Debug)] +#[derive(Debug)] pub enum Callback { /// Invoke a one-shot system System(SystemId), /// Ignore this notification + Ignore, +} + +impl Copy for Callback {} +impl Clone for Callback { + fn clone(&self) -> Self { + match self { + Self::System(arg0) => Self::System(arg0.clone()), + Self::Ignore => Self::Ignore, + } + } +} + +impl GetTemplate for Callback { + type Template = CallbackTemplate; +} + +#[derive(Default)] +pub enum CallbackTemplate { + System(Box>), + SystemId(SystemId), #[default] Ignore, } +impl CallbackTemplate { + pub fn clone(&self) -> CallbackTemplate { + match self { + CallbackTemplate::System(register_system) => { + CallbackTemplate::System(register_system.box_clone()) + } + CallbackTemplate::SystemId(system_id) => CallbackTemplate::SystemId(*system_id), + CallbackTemplate::Ignore => CallbackTemplate::Ignore, + } + } +} + +pub trait RegisterSystem: Send + Sync + 'static { + fn register_system(&mut self, world: &mut World) -> SystemId; + fn box_clone(&self) -> Box>; +} + +pub struct IntoWrapper { + into_system: Option, + marker: PhantomData (In, Marker)>, +} + +pub fn callback< + I: IntoSystem + Send + Sync + Clone + 'static, + In: SystemInput + 'static, + Marker: 'static, +>( + system: I, +) -> CallbackTemplate { + CallbackTemplate::from(IntoWrapper { + into_system: Some(system), + marker: PhantomData, + }) +} + +impl< + I: IntoSystem + Clone + Send + Sync + 'static, + In: SystemInput + 'static, + Marker: 'static, + > RegisterSystem for IntoWrapper +{ + fn register_system(&mut self, world: &mut World) -> SystemId { + world.register_system(self.into_system.take().unwrap()) + } + + fn box_clone(&self) -> Box> { + Box::new(IntoWrapper { + into_system: self.into_system.clone(), + marker: PhantomData, + }) + } +} + +impl< + I: IntoSystem + Clone + Send + Sync + 'static, + In: SystemInput + 'static, + Marker: 'static, + > From> for CallbackTemplate +{ + fn from(value: IntoWrapper) -> Self { + CallbackTemplate::System(Box::new(value)) + } +} + +impl Template for CallbackTemplate { + type Output = Callback; + + fn build( + &mut self, + entity: &mut bevy_ecs::world::EntityWorldMut, + ) -> bevy_ecs::error::Result { + Ok(match self { + CallbackTemplate::System(register) => { + let id = entity.world_scope(move |world| register.register_system(world)); + *self = CallbackTemplate::SystemId(id); + Callback::System(id) + } + CallbackTemplate::SystemId(id) => Callback::System(*id), + CallbackTemplate::Ignore => Callback::Ignore, + }) + } +} + /// Trait used to invoke a [`Callback`], unifying the API across callers. pub trait Notify { /// Invoke the callback with no arguments. diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs index 5ef0d33ef0c30..4e1087e789a88 100644 --- a/crates/bevy_core_widgets/src/core_button.rs +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -17,11 +17,12 @@ use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; use bevy_ui::{InteractionDisabled, Pressed}; use crate::{Activate, Callback, Notify}; +use bevy_ecs::template::GetTemplate; /// 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 a `ButtonClicked` /// event when the button is un-pressed. -#[derive(Component, Default, Debug)] +#[derive(Component, Clone, Debug, GetTemplate)] #[require(AccessibilityNode(accesskit::Node::new(Role::Button)))] pub struct CoreButton { /// Callback to invoke when the button is clicked, or when the `Enter` or `Space` key diff --git a/crates/bevy_core_widgets/src/core_checkbox.rs b/crates/bevy_core_widgets/src/core_checkbox.rs index 01e3e61e49acd..db7f013abb1fe 100644 --- a/crates/bevy_core_widgets/src/core_checkbox.rs +++ b/crates/bevy_core_widgets/src/core_checkbox.rs @@ -16,6 +16,7 @@ use bevy_picking::events::{Click, Pointer}; use bevy_ui::{Checkable, Checked, InteractionDisabled}; use crate::{Callback, Notify as _, ValueChange}; +use bevy_ecs::template::GetTemplate; /// Headless widget implementation for checkboxes. The [`Checked`] component represents the current /// state of the checkbox. The `on_change` field is an optional system id that will be run when the @@ -28,7 +29,7 @@ use crate::{Callback, Notify as _, ValueChange}; /// The [`CoreCheckbox`] 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, Clone, GetTemplate)] #[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)] pub struct CoreCheckbox { /// One-shot system that is run when the checkbox state needs to be changed. If this value is diff --git a/crates/bevy_core_widgets/src/core_radio.rs b/crates/bevy_core_widgets/src/core_radio.rs index 0aeebe9825cf0..ff4f10dbe3b22 100644 --- a/crates/bevy_core_widgets/src/core_radio.rs +++ b/crates/bevy_core_widgets/src/core_radio.rs @@ -17,6 +17,7 @@ use bevy_picking::events::{Click, Pointer}; use bevy_ui::{Checkable, Checked, InteractionDisabled}; use crate::{Activate, Callback, Notify}; +use bevy_ecs::template::GetTemplate; /// Headless widget implementation for a "radio button group". This component is used to group /// multiple [`CoreRadio`] components together, allowing them to behave as a single unit. It @@ -33,7 +34,7 @@ use crate::{Activate, Callback, Notify}; /// 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, GetTemplate)] #[require(AccessibilityNode(accesskit::Node::new(Role::RadioGroup)))] pub struct CoreRadioGroup { /// Callback which is called when the selected radio button changes. @@ -46,7 +47,7 @@ pub struct CoreRadioGroup { /// 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)] pub struct CoreRadio; diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index 9f38065e374bc..a1eb3b62a1177 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -24,6 +24,7 @@ use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; use crate::{Callback, Notify, ValueChange}; +use bevy_ecs::template::GetTemplate; /// Defines how the slider should behave when you click on the track (not the thumb). #[derive(Debug, Default, PartialEq, Clone, Copy)] @@ -66,7 +67,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, GetTemplate, Clone)] #[require( AccessibilityNode(accesskit::Node::new(Role::Slider)), CoreSliderDragState, diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs index 9a20b59c13032..ae751268a8b58 100644 --- a/crates/bevy_core_widgets/src/lib.rs +++ b/crates/bevy_core_widgets/src/lib.rs @@ -24,7 +24,7 @@ mod core_slider; use bevy_app::{PluginGroup, PluginGroupBuilder}; use bevy_ecs::entity::Entity; -pub use callback::{Callback, Notify}; +pub use callback::{callback, Callback, CallbackTemplate, Notify}; pub use core_button::{CoreButton, CoreButtonPlugin}; pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked}; pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin}; diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index f0f9b782afff2..b0c050cd8afd8 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -121,6 +121,7 @@ variadics_please = { version = "1.1", default-features = false } tracing = { version = "0.1", default-features = false, optional = true } log = { version = "0.4", default-features = false } bumpalo = "3" +downcast-rs = { version = "2", default-features = false, features = ["std"] } subsecond = { version = "0.7.0-alpha.1", optional = true } slotmap = { version = "1.0.7", default-features = false } diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 7b388f4a1446a..c2b541efc6c78 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -7,6 +7,8 @@ extern crate proc_macro; mod component; mod query_data; mod query_filter; +mod template; +mod variant_defaults; mod world_query; use crate::{ @@ -730,3 +732,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..2e451d58060a2 --- /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, entity: &mut #bevy_ecs::world::EntityWorldMut) -> #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, entity: &mut #bevy_ecs::world::EntityWorldMut) -> #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, entity: &mut #bevy_ecs::world::EntityWorldMut) -> #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, entity: &mut #bevy_ecs::world::EntityWorldMut) -> #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(entity)?, + #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(entity)?, + #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(entity)? + }); + } else { + template_field_builds.push(quote! { + #ident: self.#ident.build(entity)? + }); + } + + 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(entity)?, + #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(entity)?, + #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(entity)? + }); + } else { + template_field_builds.push(quote! { + self.#index.build(entity)? + }); + } + 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..a7a565fc66d31 --- /dev/null +++ b/crates/bevy_ecs/macros/src/variant_defaults.rs @@ -0,0 +1,55 @@ +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! { + 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! { + pub fn #variant_default_name() -> Self { + Self::#variant_ident( + #(#fields,)* + ) + } + }) + } + syn::Fields::Unit => variant_defaults.push(quote! { + 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..744de7ca17143 --- /dev/null +++ b/crates/bevy_ecs/src/entity/entity_path.rs @@ -0,0 +1,51 @@ +use crate::{entity::Entity, world::EntityWorldMut}; +use log::warn; +use std::{borrow::Cow, string::String}; +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 64a8c8952e0df..e17b78d0f1351 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 96fd542b61bc3..e3fffe85d1ade 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -54,6 +54,7 @@ pub mod schedule; pub mod spawn; pub mod storage; pub mod system; +pub mod template; pub mod traversal; pub mod world; @@ -104,6 +105,7 @@ pub mod prelude { Res, ResMut, Single, System, SystemIn, SystemInput, SystemParamBuilder, SystemParamFunction, When, }, + template::{GetTemplate, Template}, world::{ EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, FromWorld, World, @@ -125,6 +127,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..c9f04b63c295f --- /dev/null +++ b/crates/bevy_ecs/src/template.rs @@ -0,0 +1,190 @@ +//! Functionality that relates to the [`Template`] trait. + +pub use bevy_ecs_macros::GetTemplate; + +use crate::{ + bundle::Bundle, + entity::{Entity, EntityPath}, + error::{BevyError, Result}, + world::EntityWorldMut, +}; +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, entity: &mut EntityWorldMut) -> 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) {} +} + +/// [`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, _entity: &mut EntityWorldMut) -> 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(_entity)?,)*)) + } + + 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 EntityPath<'static> { + type Output = Entity; + + fn build(&mut self, entity: &mut EntityWorldMut) -> Result { + Ok(entity.resolve_path(self)?) + } +} + +impl GetTemplate for Entity { + type Template = EntityPath<'static>; +} + +impl Template for T { + type Output = T; + + fn build(&mut self, _entity: &mut EntityWorldMut) -> Result { + Ok(self.clone()) + } +} + +impl GetTemplate for T { + type Template = T; +} + +/// 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, entity: &mut EntityWorldMut) -> Result<(), BevyError>; +} + +impl_downcast!(ErasedTemplate); + +impl + Send + Sync + 'static> ErasedTemplate for T { + fn apply(&mut self, entity: &mut EntityWorldMut) -> Result<(), BevyError> { + let bundle = self.build(entity)?; + 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, entity: &mut EntityWorldMut) -> Result { + Ok(match self { + TemplateField::Template(value) => value.build(entity)?, + 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, entity: &mut EntityWorldMut) -> Result { + (self.0)(entity) + } +} + +/// 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 07d883704ac73..ca82edaab1f5b 100644 --- a/crates/bevy_feathers/Cargo.toml +++ b/crates/bevy_feathers/Cargo.toml @@ -16,6 +16,7 @@ bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } bevy_core_widgets = { path = "../bevy_core_widgets", version = "0.17.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_scene2 = { path = "../bevy_scene2", version = "0.17.0-dev" } bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" } bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } diff --git a/crates/bevy_feathers/src/constants.rs b/crates/bevy_feathers/src/constants.rs index 359e5a4935b0c..7e8986f324b69 100644 --- a/crates/bevy_feathers/src/constants.rs +++ b/crates/bevy_feathers/src/constants.rs @@ -21,6 +21,12 @@ pub mod size { /// Common row size for buttons, sliders, spinners, etc. pub const ROW_HEIGHT: Val = Val::Px(24.0); + /// Height for pane headers + pub const HEADER_HEIGHT: Val = Val::Px(30.0); + + /// Common size for toolbar buttons. + pub const TOOL_HEIGHT: Val = Val::Px(18.0); + /// Width and height of a checkbox pub const CHECKBOX_SIZE: Val = Val::Px(18.0); diff --git a/crates/bevy_feathers/src/containers/flex_spacer.rs b/crates/bevy_feathers/src/containers/flex_spacer.rs new file mode 100644 index 0000000000000..a3549fe4a73b2 --- /dev/null +++ b/crates/bevy_feathers/src/containers/flex_spacer.rs @@ -0,0 +1,11 @@ +use bevy_scene2::{bsn, Scene}; +use bevy_ui::Node; + +/// An invisible UI node that takes up space, and which has a positive `flex_grow` setting. +pub fn flex_spacer() -> impl Scene { + bsn! { + Node { + flex_grow: 1.0, + } + } +} diff --git a/crates/bevy_feathers/src/containers/mod.rs b/crates/bevy_feathers/src/containers/mod.rs new file mode 100644 index 0000000000000..f14fb59fbb36d --- /dev/null +++ b/crates/bevy_feathers/src/containers/mod.rs @@ -0,0 +1,8 @@ +//! Meta-module containing all feathers containers (passive widgets that hold other widgets). +mod flex_spacer; +mod pane; +mod subpane; + +pub use flex_spacer::flex_spacer; +pub use pane::{pane, pane_body, pane_header, pane_header_divider}; +pub use subpane::{subpane, subpane_body, subpane_header}; diff --git a/crates/bevy_feathers/src/containers/pane.rs b/crates/bevy_feathers/src/containers/pane.rs new file mode 100644 index 0000000000000..de8c3cb9b00ad --- /dev/null +++ b/crates/bevy_feathers/src/containers/pane.rs @@ -0,0 +1,86 @@ +use bevy_scene2::{bsn, template_value, Scene}; +use bevy_ui::{ + AlignItems, AlignSelf, Display, FlexDirection, JustifyContent, Node, PositionType, UiRect, Val, +}; + +use crate::{ + constants::{fonts, size}, + font_styles::InheritableFont, + rounded_corners::RoundedCorners, + theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, + tokens, +}; + +/// A standard pane +pub fn pane() -> impl Scene { + bsn! { + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + align_items: AlignItems::Stretch, + } + } +} + +/// Pane header +pub fn pane_header() -> impl Scene { + bsn! { + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::SpaceBetween, + padding: UiRect::axes(Val::Px(6.0), Val::Px(6.0)), + border: UiRect { + left: Val::Px(1.0), + top: Val::Px(1.0), + right: Val::Px(1.0), + bottom: Val::Px(0.0), + }, + min_height: size::HEADER_HEIGHT, + column_gap: Val::Px(6.0), + } + ThemeBackgroundColor(tokens::PANE_HEADER_BG) + ThemeBorderColor(tokens::PANE_HEADER_BORDER) + ThemeFontColor(tokens::PANE_HEADER_TEXT) + template_value(RoundedCorners::Top.to_border_radius(4.0)) + InheritableFont { + font: fonts::REGULAR, + font_size: 14.0, + } + } +} + +/// Divider between groups of widgets in pane headers +pub fn pane_header_divider() -> impl Scene { + bsn! { + Node { + width: Val::Px(1.0), + align_self: AlignSelf::Stretch, + } + [( + // Because we want to extend the divider into the header padding area, we'll use + // an absolutely-positioned child. + Node { + position_type: PositionType::Absolute, + left: Val::Px(0.0), + right: Val::Px(0.0), + top: Val::Px(-6.0), + bottom: Val::Px(-6.0), + } + ThemeBackgroundColor(tokens::PANE_HEADER_DIVIDER) + )] + } +} + +/// Pane body +pub fn pane_body() -> impl Scene { + bsn! { + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + padding: UiRect::axes(Val::Px(6.0), Val::Px(6.0)), + } + template_value(RoundedCorners::Bottom.to_border_radius(4.0)) + } +} diff --git a/crates/bevy_feathers/src/containers/subpane.rs b/crates/bevy_feathers/src/containers/subpane.rs new file mode 100644 index 0000000000000..bc4201364c011 --- /dev/null +++ b/crates/bevy_feathers/src/containers/subpane.rs @@ -0,0 +1,74 @@ +use bevy_scene2::{bsn, template_value, Scene}; +use bevy_ui::{AlignItems, Display, FlexDirection, JustifyContent, Node, UiRect, Val}; + +use crate::{ + constants::{fonts, size}, + font_styles::InheritableFont, + rounded_corners::RoundedCorners, + theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, + tokens, +}; + +/// Sub-pane +pub fn subpane() -> impl Scene { + bsn! { + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + align_items: AlignItems::Stretch, + } + } +} + +/// Sub-pane header +pub fn subpane_header() -> impl Scene { + bsn! { + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::SpaceBetween, + border: UiRect { + left: Val::Px(1.0), + top: Val::Px(1.0), + right: Val::Px(1.0), + bottom: Val::Px(0.0), + }, + padding: UiRect::axes(Val::Px(10.0), Val::Px(0.0)), + min_height: size::HEADER_HEIGHT, + column_gap: Val::Px(4.0), + } + ThemeBackgroundColor(tokens::SUBPANE_HEADER_BG) + ThemeBorderColor(tokens::SUBPANE_HEADER_BORDER) + ThemeFontColor(tokens::SUBPANE_HEADER_TEXT) + template_value(RoundedCorners::Top.to_border_radius(4.0)) + InheritableFont { + font: fonts::REGULAR, + font_size: 14.0, + } + } +} + +/// Sub-pane body +pub fn subpane_body() -> impl Scene { + bsn! { + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + border: UiRect { + left: Val::Px(1.0), + top: Val::Px(0.0), + right: Val::Px(1.0), + bottom: Val::Px(1.0), + }, + padding: UiRect::axes(Val::Px(6.0), Val::Px(6.0)), + } + ThemeBackgroundColor(tokens::SUBPANE_BODY_BG) + ThemeBorderColor(tokens::SUBPANE_BODY_BORDER) + template_value(RoundedCorners::Bottom.to_border_radius(4.0)) + InheritableFont { + font: fonts::REGULAR, + font_size: 14.0, + } + } +} diff --git a/crates/bevy_feathers/src/controls/button.rs b/crates/bevy_feathers/src/controls/button.rs index ad479f1ec5202..718df066471c6 100644 --- a/crates/bevy_feathers/src/controls/button.rs +++ b/crates/bevy_feathers/src/controls/button.rs @@ -1,29 +1,26 @@ use bevy_app::{Plugin, PreUpdate}; -use bevy_core_widgets::{Activate, Callback, CoreButton}; +use bevy_core_widgets::{Activate, CallbackTemplate, CoreButton}; use bevy_ecs::{ - bundle::Bundle, component::Component, entity::Entity, - hierarchy::{ChildOf, Children}, lifecycle::RemovedComponents, query::{Added, Changed, Has, Or}, schedule::IntoScheduleConfigs, - spawn::{SpawnRelated, SpawnableList}, system::{Commands, In, Query}, }; -use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; +use bevy_scene2::{prelude::*, template_value}; use bevy_ui::{AlignItems, InteractionDisabled, JustifyContent, Node, Pressed, UiRect, Val}; -use bevy_winit::cursor::CursorIcon; use crate::{ constants::{fonts, size}, font_styles::InheritableFont, - handle_or_path::HandleOrPath, rounded_corners::RoundedCorners, theme::{ThemeBackgroundColor, ThemeFontColor}, tokens, }; +use bevy_input_focus::tab_navigation::TabIndex; +use bevy_winit::cursor::CursorIcon; /// Color variants for buttons. This also functions as a component used by the dynamic styling /// system to identify which entities are buttons. @@ -35,6 +32,10 @@ pub enum ButtonVariant { /// A button with a more prominent color, this is used for "call to action" buttons, /// default buttons for dialog boxes, and so on. Primary, + /// For a toggle button, indicates that the button is in a "toggled" state. + Selected, + /// Don't display the button background unless hovering or pressed. + Plain, } /// Parameters for the button template, passed to [`button`] function. @@ -45,46 +46,70 @@ pub struct ButtonProps { /// Rounded corners options pub corners: RoundedCorners, /// Click handler - pub on_click: Callback>, + pub on_click: CallbackTemplate>, } -/// 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() - }, + } CoreButton { - on_activate: props.on_click, - }, - props.variant, - Hovered::default(), - CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - props.corners.to_border_radius(4.0), - ThemeBackgroundColor(tokens::BUTTON_BG), - ThemeFontColor(tokens::BUTTON_TEXT), + on_activate: {props.on_click.clone()}, + } + template_value(props.variant) + template_value(props.corners.to_border_radius(4.0)) + Hovered + // TODO: port CursonIcon to GetTemplate + // CursorIcon::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeBackgroundColor(tokens::BUTTON_BG) + ThemeFontColor(tokens::BUTTON_TEXT) 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::TOOL_HEIGHT, + min_width: size::TOOL_HEIGHT, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + padding: UiRect::axes(Val::Px(2.0), Val::Px(0.)), + } + CoreButton { + on_activate: {props.on_click.clone()}, + } + template_value(props.variant) + template_value(props.corners.to_border_radius(3.0)) + Hovered + // TODO: port CursonIcon to GetTemplate + // CursorIcon::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeBackgroundColor(tokens::BUTTON_BG) + ThemeFontColor(tokens::BUTTON_TEXT) + InheritableFont { + font: fonts::REGULAR, + font_size: 14.0, + } + } } fn update_button_styles( @@ -171,11 +196,23 @@ fn set_button_colors( (ButtonVariant::Primary, false, true, _) => tokens::BUTTON_PRIMARY_BG_PRESSED, (ButtonVariant::Primary, false, false, true) => tokens::BUTTON_PRIMARY_BG_HOVER, (ButtonVariant::Primary, false, false, false) => tokens::BUTTON_PRIMARY_BG, + (ButtonVariant::Selected, true, _, _) => tokens::BUTTON_SELECTED_BG_DISABLED, + (ButtonVariant::Selected, false, true, _) => tokens::BUTTON_SELECTED_BG_PRESSED, + (ButtonVariant::Selected, false, false, true) => tokens::BUTTON_SELECTED_BG_HOVER, + (ButtonVariant::Selected, false, false, false) => tokens::BUTTON_SELECTED_BG, + (ButtonVariant::Plain, true, _, _) => tokens::BUTTON_PLAIN_BG_DISABLED, + (ButtonVariant::Plain, false, true, _) => tokens::BUTTON_PLAIN_BG_PRESSED, + (ButtonVariant::Plain, false, false, true) => tokens::BUTTON_PLAIN_BG_HOVER, + (ButtonVariant::Plain, false, false, false) => tokens::BUTTON_PLAIN_BG, }; let font_color_token = match (variant, disabled) { - (ButtonVariant::Normal, true) => tokens::BUTTON_TEXT_DISABLED, - (ButtonVariant::Normal, false) => tokens::BUTTON_TEXT, + (ButtonVariant::Normal | ButtonVariant::Selected | ButtonVariant::Plain, true) => { + tokens::BUTTON_TEXT_DISABLED + } + (ButtonVariant::Normal | ButtonVariant::Selected | ButtonVariant::Plain, false) => { + tokens::BUTTON_TEXT + } (ButtonVariant::Primary, true) => tokens::BUTTON_PRIMARY_TEXT_DISABLED, (ButtonVariant::Primary, false) => tokens::BUTTON_PRIMARY_TEXT, }; diff --git a/crates/bevy_feathers/src/controls/checkbox.rs b/crates/bevy_feathers/src/controls/checkbox.rs index db37f82623c09..8f152398c3cf0 100644 --- a/crates/bevy_feathers/src/controls/checkbox.rs +++ b/crates/bevy_feathers/src/controls/checkbox.rs @@ -1,21 +1,19 @@ use bevy_app::{Plugin, PreUpdate}; -use bevy_core_widgets::{Callback, CoreCheckbox, ValueChange}; +use bevy_core_widgets::{CallbackTemplate, CoreCheckbox, ValueChange}; use bevy_ecs::{ - bundle::Bundle, - children, component::Component, entity::Entity, - hierarchy::{ChildOf, Children}, + hierarchy::Children, lifecycle::RemovedComponents, query::{Added, Changed, Has, Or, With}, schedule::IntoScheduleConfigs, - spawn::{Spawn, SpawnRelated, SpawnableList}, system::{Commands, In, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_math::Rot2; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_render::view::Visibility; +use bevy_scene2::prelude::*; use bevy_ui::{ AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, Node, PositionType, UiRect, UiTransform, Val, @@ -25,7 +23,6 @@ use bevy_winit::cursor::CursorIcon; use crate::{ constants::{fonts, size}, font_styles::InheritableFont, - handle_or_path::HandleOrPath, theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, tokens, }; @@ -34,7 +31,7 @@ use crate::{ #[derive(Default)] pub struct CheckboxProps { /// Change handler - pub on_change: Callback>>, + pub on_change: CallbackTemplate>>, } /// Marker for the checkbox frame (contains both checkbox and label) @@ -49,74 +46,61 @@ struct CheckboxOutline; #[derive(Component, Default, Clone)] 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>( - props: CheckboxProps, - overrides: B, - label: C, -) -> impl Bundle { - ( +pub fn checkbox(props: CheckboxProps) -> 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() - }, + } CoreCheckbox { - on_change: props.on_change, - }, - CheckboxFrame, - Hovered::default(), - CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - ThemeFontColor(tokens::CHECKBOX_TEXT), + on_change: {props.on_change.clone()}, + } + CheckboxFrame + Hovered + // TODO: port CursorIcon to GetTemplate + // CursorIcon::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/mod.rs b/crates/bevy_feathers/src/controls/mod.rs index ecad39707b925..5c35cb610c822 100644 --- a/crates/bevy_feathers/src/controls/mod.rs +++ b/crates/bevy_feathers/src/controls/mod.rs @@ -7,7 +7,7 @@ mod radio; mod slider; mod toggle_switch; -pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant}; +pub use button::{button, tool_button, ButtonPlugin, ButtonProps, ButtonVariant}; pub use checkbox::{checkbox, CheckboxPlugin, CheckboxProps}; pub use radio::{radio, RadioPlugin}; pub use slider::{slider, SliderPlugin, SliderProps}; diff --git a/crates/bevy_feathers/src/controls/radio.rs b/crates/bevy_feathers/src/controls/radio.rs index a08ffcfa8d136..0f9b7ec9582ca 100644 --- a/crates/bevy_feathers/src/controls/radio.rs +++ b/crates/bevy_feathers/src/controls/radio.rs @@ -1,20 +1,18 @@ use bevy_app::{Plugin, PreUpdate}; use bevy_core_widgets::CoreRadio; use bevy_ecs::{ - bundle::Bundle, - children, component::Component, entity::Entity, - hierarchy::{ChildOf, Children}, + hierarchy::Children, lifecycle::RemovedComponents, query::{Added, Changed, Has, Or, With}, schedule::IntoScheduleConfigs, - spawn::{Spawn, SpawnRelated, SpawnableList}, system::{Commands, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_render::view::Visibility; +use bevy_scene2::prelude::*; use bevy_ui::{ AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, Node, UiRect, Val, @@ -24,7 +22,6 @@ use bevy_winit::cursor::CursorIcon; use crate::{ constants::{fonts, size}, font_styles::InheritableFont, - handle_or_path::HandleOrPath, theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, tokens, }; @@ -37,64 +34,50 @@ struct RadioOutline; #[derive(Component, Default, Clone)] 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() - }, - CoreRadio, - Hovered::default(), - CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - ThemeFontColor(tokens::RADIO_TEXT), + } + CoreRadio + Hovered + // TODO: port CursorIcon to GetTemplate + // CursorIcon::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 228801b85cf3e..915da874b3358 100644 --- a/crates/bevy_feathers/src/controls/slider.rs +++ b/crates/bevy_feathers/src/controls/slider.rs @@ -2,21 +2,21 @@ use core::f32::consts::PI; use bevy_app::{Plugin, PreUpdate}; use bevy_color::Color; -use bevy_core_widgets::{Callback, CoreSlider, SliderRange, SliderValue, TrackClick, ValueChange}; +use bevy_core_widgets::{ + CallbackTemplate, CoreSlider, SliderRange, SliderValue, TrackClick, ValueChange, +}; use bevy_ecs::{ - bundle::Bundle, - children, component::Component, entity::Entity, hierarchy::Children, lifecycle::RemovedComponents, query::{Added, Changed, Has, Or, Spawned, With}, schedule::IntoScheduleConfigs, - spawn::SpawnRelated, system::{In, Query, Res}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::PickingSystems; +use bevy_scene2::{prelude::*, template_value}; use bevy_ui::{ widget::Text, AlignItems, BackgroundGradient, ColorStop, Display, FlexDirection, Gradient, InteractionDisabled, InterpolationColorSpace, JustifyContent, LinearGradient, Node, UiRect, @@ -27,7 +27,6 @@ use bevy_winit::cursor::CursorIcon; use crate::{ constants::{fonts, size}, font_styles::InheritableFont, - handle_or_path::HandleOrPath, rounded_corners::RoundedCorners, theme::{ThemeFontColor, ThemedText, UiTheme}, tokens, @@ -42,7 +41,7 @@ pub struct SliderProps { /// Slider maximum value pub max: f32, /// On-change handler - pub on_change: Callback>>, + pub on_change: CallbackTemplate>>, } impl Default for SliderProps { @@ -51,47 +50,45 @@ impl Default for SliderProps { value: 0.0, min: 0.0, max: 1.0, - on_change: Callback::Ignore, + on_change: CallbackTemplate::Ignore, } } } #[derive(Component, Default, Clone)] -#[require(CoreSlider)] struct SliderStyle; /// Marker for the text #[derive(Component, Default, Clone)] 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() - }, + } CoreSlider { - on_change: props.on_change, + on_change: {props.on_change.clone()}, track_click: TrackClick::Drag, - }, - SliderStyle, - SliderValue(props.value), - SliderRange::new(props.min, props.max), - CursorIcon::System(bevy_window::SystemCursorIcon::EwResize), - TabIndex(0), - RoundedCorners::All.to_border_radius(6.0), + } + SliderStyle + SliderValue({props.value}) + SliderRange::new(props.min, props.max) + // TODO: port CursorIcon to GetTemplate + // CursorIcon::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.)), @@ -100,25 +97,23 @@ pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { ColorStop::new(Color::NONE, Val::Percent(100.)), ], color_space: InterpolationColorSpace::Srgb, - })]), - overrides, - children![( + })]}) + [( // Text container Node { display: Display::Flex, 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_colors( diff --git a/crates/bevy_feathers/src/controls/toggle_switch.rs b/crates/bevy_feathers/src/controls/toggle_switch.rs index e3437a829d6a5..2d43625e48cf0 100644 --- a/crates/bevy_feathers/src/controls/toggle_switch.rs +++ b/crates/bevy_feathers/src/controls/toggle_switch.rs @@ -1,24 +1,21 @@ use accesskit::Role; use bevy_a11y::AccessibilityNode; use bevy_app::{Plugin, PreUpdate}; -use bevy_core_widgets::{Callback, CoreCheckbox, ValueChange}; +use bevy_core_widgets::{Callback, CallbackTemplate, CoreCheckbox, ValueChange}; use bevy_ecs::{ - bundle::Bundle, - children, component::Component, entity::Entity, hierarchy::Children, lifecycle::RemovedComponents, query::{Added, Changed, Has, Or, With}, schedule::IntoScheduleConfigs, - spawn::SpawnRelated, system::{Commands, In, Query}, world::Mut, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; +use bevy_scene2::prelude::*; use bevy_ui::{BorderRadius, Checked, InteractionDisabled, Node, PositionType, UiRect, Val}; -use bevy_winit::cursor::CursorIcon; use crate::{ constants::size, @@ -30,7 +27,7 @@ use crate::{ #[derive(Default)] pub struct ToggleSwitchProps { /// Change handler - pub on_change: Callback>>, + pub on_change: CallbackTemplate>>, } /// Marker for the toggle switch outline @@ -41,45 +38,42 @@ struct ToggleSwitchOutline; #[derive(Component, Default, Clone)] 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(props: ToggleSwitchProps, overrides: B) -> impl Bundle { - ( +pub fn toggle_switch(props: ToggleSwitchProps) -> impl Scene { + bsn! { Node { width: size::TOGGLE_WIDTH, height: size::TOGGLE_HEIGHT, border: UiRect::all(Val::Px(2.0)), - ..Default::default() - }, + } CoreCheckbox { - on_change: props.on_change, - }, - ToggleSwitchOutline, - BorderRadius::all(Val::Px(5.0)), - ThemeBackgroundColor(tokens::SWITCH_BG), - ThemeBorderColor(tokens::SWITCH_BORDER), - AccessibilityNode(accesskit::Node::new(Role::Switch)), - Hovered::default(), - CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - overrides, - children![( + on_change: {props.on_change.clone()}, + } + ToggleSwitchOutline + BorderRadius::all(Val::Px(5.0)) + ThemeBackgroundColor(tokens::SWITCH_BG) + ThemeBorderColor(tokens::SWITCH_BORDER) + AccessibilityNode(accesskit::Node::new(Role::Switch)) + Hovered + // TODO: port CursorIcon to GetTemplate + // CursorIcon::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/dark_theme.rs b/crates/bevy_feathers/src/dark_theme.rs index c3ff4e42040eb..1fdcfe1b1edc2 100644 --- a/crates/bevy_feathers/src/dark_theme.rs +++ b/crates/bevy_feathers/src/dark_theme.rs @@ -1,6 +1,6 @@ //! The standard `bevy_feathers` dark theme. use crate::{palette, tokens}; -use bevy_color::{Alpha, Luminance}; +use bevy_color::{Alpha, Color, Luminance}; use bevy_platform::collections::HashMap; use crate::theme::ThemeProps; @@ -10,7 +10,7 @@ pub fn create_dark_theme() -> ThemeProps { ThemeProps { color: HashMap::from([ (tokens::WINDOW_BG.into(), palette::GRAY_0), - // Button + // Button (normal) (tokens::BUTTON_BG.into(), palette::GRAY_3), ( tokens::BUTTON_BG_HOVER.into(), @@ -21,6 +21,7 @@ pub fn create_dark_theme() -> ThemeProps { palette::GRAY_3.lighter(0.1), ), (tokens::BUTTON_BG_DISABLED.into(), palette::GRAY_2), + // Button (primary) (tokens::BUTTON_PRIMARY_BG.into(), palette::ACCENT), ( tokens::BUTTON_PRIMARY_BG_HOVER.into(), @@ -31,6 +32,23 @@ pub fn create_dark_theme() -> ThemeProps { palette::ACCENT.lighter(0.1), ), (tokens::BUTTON_PRIMARY_BG_DISABLED.into(), palette::GRAY_2), + // Button (selected) + (tokens::BUTTON_SELECTED_BG.into(), palette::GRAY_3), + ( + tokens::BUTTON_SELECTED_BG_HOVER.into(), + palette::GRAY_3.lighter(0.05), + ), + ( + tokens::BUTTON_SELECTED_BG_PRESSED.into(), + palette::GRAY_3.lighter(0.1), + ), + (tokens::BUTTON_SELECTED_BG_DISABLED.into(), palette::GRAY_2), + // Button (plain) + (tokens::BUTTON_PLAIN_BG.into(), Color::NONE), + (tokens::BUTTON_PLAIN_BG_HOVER.into(), palette::GRAY_2), + (tokens::BUTTON_PLAIN_BG_PRESSED.into(), palette::GRAY_3), + (tokens::BUTTON_PLAIN_BG_DISABLED.into(), Color::NONE), + // Button text (tokens::BUTTON_TEXT.into(), palette::WHITE), ( tokens::BUTTON_TEXT_DISABLED.into(), @@ -122,6 +140,17 @@ pub fn create_dark_theme() -> ThemeProps { tokens::SWITCH_SLIDE_DISABLED.into(), palette::LIGHT_GRAY_2.with_alpha(0.3), ), + // Pane + (tokens::PANE_HEADER_BG.into(), palette::GRAY_0), + (tokens::PANE_HEADER_BORDER.into(), palette::WARM_GRAY_1), + (tokens::PANE_HEADER_TEXT.into(), palette::LIGHT_GRAY_1), + (tokens::PANE_HEADER_DIVIDER.into(), palette::WARM_GRAY_1), + // Subpane + (tokens::SUBPANE_HEADER_BG.into(), palette::GRAY_2), + (tokens::SUBPANE_HEADER_BORDER.into(), palette::GRAY_3), + (tokens::SUBPANE_HEADER_TEXT.into(), palette::LIGHT_GRAY_1), + (tokens::SUBPANE_BODY_BG.into(), palette::GRAY_1), + (tokens::SUBPANE_BODY_BORDER.into(), palette::GRAY_2), ]), } } diff --git a/crates/bevy_feathers/src/font_styles.rs b/crates/bevy_feathers/src/font_styles.rs index 6de064dd39248..9d45c6a40278f 100644 --- a/crates/bevy_feathers/src/font_styles.rs +++ b/crates/bevy_feathers/src/font_styles.rs @@ -1,62 +1,37 @@ //! A framework for inheritable font styles. use bevy_app::Propagate; -use bevy_asset::{AssetServer, Handle}; +use bevy_asset::Handle; use bevy_ecs::{ component::Component, lifecycle::Insert, observer::On, - system::{Commands, Query, Res}, + system::{Commands, Query}, + template::GetTemplate, }; use bevy_text::{Font, TextFont}; -use crate::handle_or_path::HandleOrPath; - /// 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`](crate::theme::ThemedText) marker. -#[derive(Component, Default, Clone, Debug)] +#[derive(Component, Clone, Debug, GetTemplate)] 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 /// propagates downward the font to all participating text entities. pub(crate) fn on_changed_font( ev: On, font_style: Query<&InheritableFont>, - assets: Res, mut commands: Commands, ) { - if let Ok(style) = font_style.get(ev.target()) { - if let Some(font) = match style.font { - HandleOrPath::Handle(ref h) => Some(h.clone()), - HandleOrPath::Path(ref p) => Some(assets.load::(p)), - } { - commands.entity(ev.target()).insert(Propagate(TextFont { - font, - font_size: style.font_size, - ..Default::default() - })); - } + if let Ok(inheritable_font) = font_style.get(ev.target()) { + commands.entity(ev.target()).insert(Propagate(TextFont { + font: inheritable_font.font.clone(), + font_size: inheritable_font.font_size, + ..Default::default() + })); } } diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs index ab02304a85b30..b07e4ea6e1583 100644 --- a/crates/bevy_feathers/src/lib.rs +++ b/crates/bevy_feathers/src/lib.rs @@ -31,6 +31,7 @@ use crate::{ }; pub mod constants; +pub mod containers; pub mod controls; pub mod cursor; pub mod dark_theme; diff --git a/crates/bevy_feathers/src/theme.rs b/crates/bevy_feathers/src/theme.rs index 9969b54846667..56f6cd3192936 100644 --- a/crates/bevy_feathers/src/theme.rs +++ b/crates/bevy_feathers/src/theme.rs @@ -49,26 +49,26 @@ impl UiTheme { } /// Component which causes the background color of an entity to be set based on a theme color. -#[derive(Component, Clone, Copy)] +#[derive(Component, Clone, Copy, Default)] #[require(BackgroundColor)] #[component(immutable)] pub struct ThemeBackgroundColor(pub &'static str); /// 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, Copy)] +#[derive(Component, Clone, Copy, Default)] #[require(BorderColor)] #[component(immutable)] pub struct ThemeBorderColor(pub &'static str); /// Component which causes the inherited text color of an entity to be set based on a theme color. -#[derive(Component, Clone, Copy)] +#[derive(Component, Clone, Copy, Default)] #[component(immutable)] pub struct ThemeFontColor(pub &'static str); /// A marker component that is used to indicate that the text entity wants to opt-in to using /// inherited text styles. -#[derive(Component)] +#[derive(Component, Default, Clone)] pub struct ThemedText; pub(crate) fn update_theme( diff --git a/crates/bevy_feathers/src/tokens.rs b/crates/bevy_feathers/src/tokens.rs index 453dc94c5ea37..9edf41b40ec06 100644 --- a/crates/bevy_feathers/src/tokens.rs +++ b/crates/bevy_feathers/src/tokens.rs @@ -45,6 +45,28 @@ pub const BUTTON_PRIMARY_TEXT: &str = "feathers.button.primary.txt"; /// Primary button text (disabled) pub const BUTTON_PRIMARY_TEXT_DISABLED: &str = "feathers.button.primary.txt.disabled"; +// Selected ("toggled") buttons + +/// Selected button background +pub const BUTTON_SELECTED_BG: &str = "feathers.button.selected.bg"; +/// Selected button background (hovered) +pub const BUTTON_SELECTED_BG_HOVER: &str = "feathers.button.selected.bg.hover"; +/// Selected button background (disabled) +pub const BUTTON_SELECTED_BG_DISABLED: &str = "feathers.button.selected.bg.disabled"; +/// Selected button background (pressed) +pub const BUTTON_SELECTED_BG_PRESSED: &str = "feathers.button.selected.bg.pressed"; + +// Plain buttons (transparent background) + +/// Plain button background +pub const BUTTON_PLAIN_BG: &str = "feathers.button.plain.bg"; +/// Plain button background (hovered) +pub const BUTTON_PLAIN_BG_HOVER: &str = "feathers.button.plain.bg.hover"; +/// Plain button background (disabled) +pub const BUTTON_PLAIN_BG_DISABLED: &str = "feathers.button.plain.bg.disabled"; +/// Plain button background (pressed) +pub const BUTTON_PLAIN_BG_PRESSED: &str = "feathers.button.plain.bg.pressed"; + // Slider /// Background for slider @@ -120,3 +142,27 @@ pub const SWITCH_BORDER_DISABLED: &str = "feathers.switch.border.disabled"; pub const SWITCH_SLIDE: &str = "feathers.switch.slide"; /// Switch slide (disabled) pub const SWITCH_SLIDE_DISABLED: &str = "feathers.switch.slide.disabled"; + +// Pane + +/// Pane header background +pub const PANE_HEADER_BG: &str = "feathers.pane.header.bg"; +/// Pane header border +pub const PANE_HEADER_BORDER: &str = "feathers.pane.header.border"; +/// Pane header text color +pub const PANE_HEADER_TEXT: &str = "feathers.pane.header.text"; +/// Pane header divider color +pub const PANE_HEADER_DIVIDER: &str = "feathers.pane.header.divider"; + +// Subpane + +/// Subpane background +pub const SUBPANE_HEADER_BG: &str = "feathers.subpane.header.bg"; +/// Subpane header border +pub const SUBPANE_HEADER_BORDER: &str = "feathers.subpane.header.border"; +/// Subpane header text color +pub const SUBPANE_HEADER_TEXT: &str = "feathers.subpane.header.text"; +/// Subpane body background +pub const SUBPANE_BODY_BG: &str = "feathers.subpane.body.bg"; +/// Subpane body border +pub const SUBPANE_BODY_BORDER: &str = "feathers.subpane.body.border"; diff --git a/crates/bevy_gizmos/src/retained.rs b/crates/bevy_gizmos/src/retained.rs index 4cc75f236da13..97e51c41322c6 100644 --- a/crates/bevy_gizmos/src/retained.rs +++ b/crates/bevy_gizmos/src/retained.rs @@ -4,7 +4,7 @@ use core::ops::{Deref, DerefMut}; use bevy_asset::Handle; 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")] @@ -72,8 +72,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. @@ -95,6 +95,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 67e1b203170a0..111d912d5c298 100644 --- a/crates/bevy_image/src/texture_atlas.rs +++ b/crates/bevy_image/src/texture_atlas.rs @@ -204,7 +204,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), @@ -217,6 +217,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_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index e591803751f7d..ca99a50d39389 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -443,6 +443,7 @@ bevy_picking = { path = "../bevy_picking", optional = true, version = "0.17.0-de bevy_remote = { path = "../bevy_remote", optional = true, version = "0.17.0-dev" } bevy_render = { path = "../bevy_render", optional = true, version = "0.17.0-dev" } bevy_scene = { path = "../bevy_scene", optional = true, version = "0.17.0-dev" } +bevy_scene2 = { path = "../bevy_scene2", optional = true, version = "0.17.0-dev" } bevy_solari = { path = "../bevy_solari", optional = true, version = "0.17.0-dev" } bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.17.0-dev" } bevy_state = { path = "../bevy_state", optional = true, version = "0.17.0-dev", default-features = false, features = [ diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index cdb59921dcc74..d52856cb16b21 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -25,6 +25,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, #[cfg(feature = "bevy_render")] diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 4f965e603a76f..292d073729c65 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -70,6 +70,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_solari")] pub use bevy_solari as solari; #[cfg(feature = "bevy_sprite")] diff --git a/crates/bevy_mesh/src/components.rs b/crates/bevy_mesh/src/components.rs index cff5eab7e477f..635701fd8bba2 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 682fac09c053e..3bd6f3b96f2ca 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 ad280e054f67d..10e5c83c0139d 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -452,10 +452,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 3b1a836ecf83d..5cd4d177fcd90 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_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 e94cf27cd32c8..0e46507083366 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(Clone, Debug, Eq, PartialEq, Default)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct VertexState { /// The compiled shader module for this stage. pub shader: Handle, @@ -124,8 +124,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, @@ -137,8 +148,19 @@ 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(), + } + } +} + /// Describes a compute pipeline. -#[derive(Clone, Debug, PartialEq, Eq, Default)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ComputePipelineDescriptor { pub label: Option>, pub layout: Vec, @@ -153,3 +175,17 @@ pub struct ComputePipelineDescriptor { /// If this is false, reading from workgroup variables before writing to them will result in garbage values. 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(), + } + } +} diff --git a/crates/bevy_scene/src/components.rs b/crates/bevy_scene/src/components.rs index d4d42c3a1c98c..0c2d98962ce4b 100644 --- a/crates/bevy_scene/src/components.rs +++ b/crates/bevy_scene/src/components.rs @@ -12,16 +12,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)] #[cfg_attr(feature = "bevy_render", 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)] #[cfg_attr(feature = "bevy_render", 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..8363bab9a6dbd --- /dev/null +++ b/crates/bevy_scene2/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "bevy_scene2" +version = "0.17.0-dev" +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..0b29c88c0247a --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/codegen.rs @@ -0,0 +1,371 @@ +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 syn::{Ident, Index, Lit, Member, Path}; + +impl BsnRoot { + pub fn to_tokens(&self, bevy_scene: &Path, bevy_ecs: &Path, bevy_asset: &Path) -> TokenStream { + self.0.to_tokens(bevy_scene, bevy_ecs, bevy_asset) + } +} + +impl Bsn { + pub fn to_tokens(&self, bevy_scene: &Path, bevy_ecs: &Path, bevy_asset: &Path) -> 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, + ); + let path = &bsn_type.path; + quote! { + <#path as #bevy_scene::PatchTemplate>::patch_template(move |value| { + #(#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, + ); + let path = &bsn_type.path; + quote! { + <#path as #bevy_scene::PatchGetTemplate>::patch(move |value| { + #(#assignments)* + }) + } + } + BsnEntry::TemplateConst{ type_path, const_ident} => { + quote!{ + <#type_path as #bevy_scene::PatchTemplate>::patch_template( + move |value| { + *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| { + *value = #type_path::#function(#args); + }, + ) + } + } + BsnEntry::GetTemplateConstructor(BsnConstructor {type_path, function, args})=> { + quote! { + <#type_path as #bevy_scene::PatchGetTemplate>::patch( + move |value| { + *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); + 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); + 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(); + quote! { + <#bevy_ecs::name::Name as PatchGetTemplate>::patch( + move |value| { + *value = Name(#name.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, + ) { + 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); + 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); + 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::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, + ); + } + } + } + } + } + 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::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, + ); + } + } + } + } + } + } + } + } +} + +impl BsnSceneListItems { + pub fn to_tokens(&self, bevy_scene: &Path, bevy_ecs: &Path, bevy_asset: &Path) -> TokenStream { + let scenes = self + .0 + .iter() + .map(|scene| match scene { + BsnSceneListItem::Scene(bsn) => { + let tokens = bsn.to_tokens(bevy_scene, bevy_ecs, bevy_asset); + quote!{#bevy_scene::EntityScene(#tokens)} + }, + BsnSceneListItem::Expression(block) => quote!{#block}, + }); + quote! { + (#(#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); + } + }; + } +} 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..3e308604a64a0 --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/mod.rs @@ -0,0 +1,26 @@ +use crate::bsn::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"); + TokenStream::from(scene.to_tokens(&bevy_scene, &bevy_ecs, &bevy_asset)) +} + +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"); + TokenStream::from(scene.to_tokens(&bevy_scene, &bevy_ecs, &bevy_asset)) +} 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..0306f763642e0 --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/parse.rs @@ -0,0 +1,453 @@ +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::()?; + 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) { + BsnSceneListItem::Expression(input.parse::()?) + } 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 { + 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..81c1484a7a553 --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/types.rs @@ -0,0 +1,99 @@ +use proc_macro2::TokenStream; +use syn::{punctuated::Punctuated, Block, Expr, Ident, Lit, LitStr, Path, Token}; + +#[derive(Debug)] +pub struct BsnRoot(pub Bsn); + +#[derive(Debug)] +pub struct Bsn { + pub entries: Vec, +} + +#[derive(Debug)] +pub enum BsnEntry { + Name(Ident), + 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(Block), +} + +#[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), +} 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..cb8ed3e0c7de2 --- /dev/null +++ b/crates/bevy_scene2/src/lib.rs @@ -0,0 +1,97 @@ +#![allow(missing_docs)] + +pub mod prelude { + pub use crate::{ + bsn, bsn_list, on, CommandsSpawnScene, LoadScene, PatchGetTemplate, PatchTemplate, Scene, + SceneList, ScenePatchInstance, 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}; +use std::marker::PhantomData; + +#[derive(Default)] +pub struct ScenePlugin; + +impl Plugin for ScenePlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_asset::() + .add_systems(Update, (resolve_scene_patches, spawn_queued).chain()); + } +} + +/// 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, entity: &mut EntityWorldMut) -> Result { + entity.observe(self.0.clone()); + Ok(()) + } +} + +impl< + I: IntoObserverSystem + Clone + Send + Sync, + E: EntityEvent, + B: Bundle, + M: 'static, + > Scene for OnTemplate +{ + fn patch( + &self, + _assets: &AssetServer, + _patches: &bevy_asset::Assets, + 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) +} diff --git a/crates/bevy_scene2/src/resolved_scene.rs b/crates/bevy_scene2/src/resolved_scene.rs new file mode 100644 index 0000000000000..01f68b03265fb --- /dev/null +++ b/crates/bevy_scene2/src/resolved_scene.rs @@ -0,0 +1,78 @@ +use bevy_ecs::{ + bundle::Bundle, + entity::Entity, + error::Result, + relationship::Relationship, + template::{ErasedTemplate, Template}, + 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, +} + +impl ResolvedScene { + pub fn spawn(&mut self, entity: &mut EntityWorldMut) -> Result { + for template in self.templates.iter_mut() { + template.apply(entity)?; + } + + for related in self.related.values_mut() { + let target = entity.id(); + entity.world_scope(|world| -> Result { + for scene in &mut related.scenes { + let mut entity = world.spawn_empty(); + (related.insert)(&mut entity, target); + // PERF: this will result in an archetype move + scene.spawn(&mut entity)?; + } + 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 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..75c77d7f0f8b4 --- /dev/null +++ b/crates/bevy_scene2/src/scene.rs @@ -0,0 +1,169 @@ +use crate::{ResolvedRelatedScenes, ResolvedScene, SceneList, ScenePatch}; +use bevy_asset::{AssetPath, AssetServer, Assets}; +use bevy_ecs::{ + bundle::Bundle, + error::Result, + relationship::Relationship, + template::{FnTemplate, GetTemplate, Template}, + world::EntityWorldMut, +}; +use std::{any::TypeId, marker::PhantomData}; +use variadics_please::all_tuples; + +pub trait Scene: Send + Sync + 'static { + fn patch(&self, assets: &AssetServer, patches: &Assets, scene: &mut ResolvedScene); + fn register_dependencies(&self, _dependencies: &mut Vec>) {} +} + +macro_rules! scene_impl { + ($($patch: ident),*) => { + impl<$($patch: Scene),*> Scene for ($($patch,)*) { + fn patch(&self, _assets: &AssetServer, _patches: &Assets, _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(_assets, _patches, _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| { + *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) + Send + Sync + 'static, + T: Template + Send + Sync + Default + 'static, + > Scene for TemplatePatch +{ + fn patch( + &self, + _assets: &AssetServer, + _patches: &Assets, + scene: &mut ResolvedScene, + ) { + let template = scene.get_or_insert_template::(); + (self.0)(template); + } +} + +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, assets: &AssetServer, patches: &Assets, scene: &mut ResolvedScene) { + let related = scene + .related + .entry(TypeId::of::()) + .or_insert_with(ResolvedRelatedScenes::new::); + self.related_template_list + .patch_list(assets, patches, &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, assets: &AssetServer, patches: &Assets, scene: &mut ResolvedScene) { + self.0.patch(assets, patches, 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, assets: &AssetServer, patches: &Assets, scene: &mut ResolvedScene) { + let id = assets.get_path_id(&self.0).unwrap(); + let scene_patch = patches.get(id.typed()).unwrap(); + scene_patch.patch.patch(assets, patches, 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, + _assets: &AssetServer, + _patches: &Assets, + scene: &mut ResolvedScene, + ) { + scene.push_template(FnTemplate(self.0.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..8047767f52fe1 --- /dev/null +++ b/crates/bevy_scene2/src/scene_list.rs @@ -0,0 +1,101 @@ +use crate::{ResolvedScene, Scene, ScenePatch}; +use bevy_asset::{AssetPath, AssetServer, Assets}; +use variadics_please::all_tuples; + +pub trait SceneList: Send + Sync + 'static { + fn patch_list( + &self, + assets: &AssetServer, + patches: &Assets, + scenes: &mut Vec, + ); + + fn register_dependencies(&self, dependencies: &mut Vec>); +} + +pub struct EntityScene(pub S); + +impl SceneList for EntityScene { + fn patch_list( + &self, + assets: &AssetServer, + patches: &Assets, + scenes: &mut Vec, + ) { + let mut resolved_scene = ResolvedScene::default(); + self.0.patch(assets, patches, &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, _assets: &AssetServer, _patches: &Assets, _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(_assets, _patches, _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, + assets: &AssetServer, + patches: &Assets, + scenes: &mut Vec, + ) { + for scene in self { + let mut resolved_scene = ResolvedScene::default(); + scene.patch(assets, patches, &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, + assets: &AssetServer, + patches: &Assets, + scenes: &mut Vec, + ) { + for scene in self { + let mut resolved_scene = ResolvedScene::default(); + scene.patch(assets, patches, &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..cc5c090feaeac --- /dev/null +++ b/crates/bevy_scene2/src/scene_patch.rs @@ -0,0 +1,33 @@ +use crate::{ResolvedScene, Scene}; +use bevy_asset::{Asset, AssetServer, Handle, UntypedHandle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Component; +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, +} + +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, + } + } +} + +#[derive(Component, Deref, DerefMut)] +pub struct ScenePatchInstance(pub Handle); diff --git a/crates/bevy_scene2/src/spawn.rs b/crates/bevy_scene2/src/spawn.rs new file mode 100644 index 0000000000000..e40c8e773c7fe --- /dev/null +++ b/crates/bevy_scene2/src/spawn.rs @@ -0,0 +1,107 @@ +use crate::{ResolvedScene, Scene, ScenePatch, ScenePatchInstance}; +use bevy_asset::{AssetEvent, AssetId, AssetServer, Assets}; +use bevy_ecs::{event::EventCursor, prelude::*}; +use bevy_platform::collections::HashMap; + +pub trait SpawnScene { + fn spawn_scene(&mut self, scene: S) -> EntityWorldMut; +} + +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)) + } +} + +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: EventReader>, + assets: Res, + mut patches: ResMut>, +) { + for event in events.read() { + match *event { + // TODO: handle modified? + AssetEvent::LoadedWithDependencies { id } => { + let mut scene = ResolvedScene::default(); + // TODO: real error handling + let patch = patches.get(id).unwrap(); + patch.patch.patch(&assets, &patches, &mut scene); + let patch = patches.get_mut(id).unwrap(); + patch.resolved = Some(scene) + } + _ => {} + } + } +} + +#[derive(Resource, Default)] +pub struct QueuedScenes { + waiting_entities: HashMap, Vec>, +} + +pub fn spawn_queued( + world: &mut World, + handles: &mut QueryState<(Entity, &ScenePatchInstance), Added>, + mut reader: Local>>, +) { + world.resource_scope(|world, mut patches: Mut>| { + world.resource_scope(|world, mut queued: Mut| { + world.resource_scope(|world, events: Mut>>| { + for (entity, id) in handles + .iter(world) + .map(|(e, h)| (e, h.id())) + .collect::>() + { + if let Some(scene) = patches.get_mut(id).and_then(|p| p.resolved.as_mut()) { + let mut entity_mut = world.get_entity_mut(entity).unwrap(); + scene.spawn(&mut entity_mut).unwrap(); + } else { + let entities = queued.waiting_entities.entry(id).or_default(); + entities.push(entity); + } + } + + for event in reader.read(&events) { + if let AssetEvent::LoadedWithDependencies { id } = event { + let Some(scene) = patches.get_mut(*id).and_then(|p| p.resolved.as_mut()) + else { + continue; + }; + + let Some(entities) = queued.waiting_entities.remove(id) else { + continue; + }; + + for entity in entities { + let mut entity_mut = world.get_entity_mut(entity).unwrap(); + scene.spawn(&mut entity_mut).unwrap(); + } + } + } + }); + }); + }); +} diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index f71d8c63f7e80..8b478bb68bfa2 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -453,10 +453,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/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 39e215df0406e..4d06700bc8457 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -14,7 +14,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, SyncToRenderWorld, VisibilityClass, Anchor)] #[reflect(Component, Default, Debug, Clone)] #[component(on_add = view::add_visibility_class::)] @@ -42,6 +42,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/src/tilemap_chunk/mod.rs b/crates/bevy_sprite/src/tilemap_chunk/mod.rs index 174816154bc6e..dc45d459338e4 100644 --- a/crates/bevy_sprite/src/tilemap_chunk/mod.rs +++ b/crates/bevy_sprite/src/tilemap_chunk/mod.rs @@ -45,7 +45,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)] +#[derive(Component, Clone, Debug)] #[require(Anchor)] #[component(immutable, on_insert = on_insert_tilemap_chunk)] pub struct TilemapChunk { @@ -60,6 +60,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(), + } + } +} + /// Component storing the indices of tiles within a chunk. /// Each index corresponds to a specific tile in the tileset. #[derive(Component, Clone, Debug, Deref, DerefMut)] diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 330f0d977a279..0a940f933864d 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -343,7 +343,7 @@ impl 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 b50f4cc245a64..9d6ee2d67ab9f 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(trigger: On, mut world: DeferredWorld) { diff --git a/crates/bevy_winit/src/custom_cursor.rs b/crates/bevy_winit/src/custom_cursor.rs index dd8236e30e43d..b441c6a14b623 100644 --- a/crates/bevy_winit/src/custom_cursor.rs +++ b/crates/bevy_winit/src/custom_cursor.rs @@ -8,7 +8,7 @@ use wgpu_types::TextureFormat; use crate::{cursor::CursorIcon, state::CustomCursorCache}; /// A custom cursor created from an image. -#[derive(Debug, Clone, Default, Reflect, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash)] #[reflect(Debug, Default, Hash, PartialEq, Clone)] pub struct CustomCursorImage { /// Handle to the image to use as the cursor. The image must be in 8 bit int @@ -42,6 +42,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(), + } + } +} + #[cfg(all(target_family = "wasm", target_os = "unknown"))] /// A custom cursor created from a URL. #[derive(Debug, Clone, Default, Reflect, PartialEq, Eq, Hash)] diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index 25106adcfb48f..4a657d14d4736 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 d0aa9c8680690..c2fabe0bfe8c5 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..22d73259fd94b --- /dev/null +++ b/examples/scene/bsn.rs @@ -0,0 +1,197 @@ +//! 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(|event: 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(Event, EntityEvent)] +struct Explode; + +#[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 2e8a68320ec94..ccf138f848819 100644 --- a/examples/ui/feathers.rs +++ b/examples/ui/feathers.rs @@ -1,14 +1,19 @@ //! This example shows off the various Bevy Feathers widgets. use bevy::{ + color::palettes, core_widgets::{ - Activate, Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision, + callback, Activate, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision, SliderStep, }, feathers::{ + containers::{ + flex_spacer, pane, pane_body, pane_header, pane_header_divider, subpane, subpane_body, + subpane_header, + }, controls::{ - button, checkbox, radio, slider, toggle_switch, ButtonProps, ButtonVariant, - CheckboxProps, SliderProps, ToggleSwitchProps, + button, checkbox, radio, slider, toggle_switch, tool_button, ButtonProps, + ButtonVariant, CheckboxProps, SliderProps, ToggleSwitchProps, }, dark_theme::create_dark_theme, rounded_corners::RoundedCorners, @@ -20,6 +25,7 @@ use bevy::{ InputDispatchPlugin, }, prelude::*, + scene2::prelude::{Scene, *}, ui::{Checked, InteractionDisabled}, winit::WinitSettings, }; @@ -43,38 +49,23 @@ fn main() { fn setup(mut commands: Commands) { // ui camera commands.spawn(Camera2d); - let root = demo_root(&mut commands); - commands.spawn(root); + commands.spawn_scene(demo_root()); } -fn demo_root(commands: &mut Commands) -> impl Bundle { - // Update radio button states based on notification from radio group. - let radio_exclusion = commands.register_system( - |ent: In, q_radio: Query>, mut commands: Commands| { - for radio in q_radio.iter() { - if radio == ent.0 .0 { - commands.entity(radio).insert(Checked); - } else { - commands.entity(radio).remove::(); - } - } - }, - ); - - ( +fn demo_root() -> impl Scene { + bsn! { Node { width: Val::Percent(100.0), height: Val::Percent(100.0), align_items: AlignItems::Start, justify_content: JustifyContent::Start, display: Display::Flex, - flex_direction: FlexDirection::Column, - row_gap: Val::Px(10.0), - ..default() - }, - TabGroup::default(), - ThemeBackgroundColor(tokens::WINDOW_BG), - children![( + flex_direction: FlexDirection::Row, + column_gap: Val::Px(10.0), + } + TabGroup + ThemeBackgroundColor(tokens::WINDOW_BG) + [ Node { display: Display::Flex, flex_direction: FlexDirection::Column, @@ -84,199 +75,229 @@ fn demo_root(commands: &mut Commands) -> impl Bundle { row_gap: Val::Px(8.0), width: Val::Percent(30.), min_width: Val::Px(200.), - ..default() - }, - children![ - ( - Node { - display: Display::Flex, - flex_direction: FlexDirection::Row, - align_items: AlignItems::Center, - justify_content: JustifyContent::Start, - column_gap: Val::Px(8.0), - ..default() - }, - children![ - button( - ButtonProps { - on_click: Callback::System(commands.register_system( - |_: In| { - info!("Normal button clicked!"); - } - )), - ..default() - }, - (), - Spawn((Text::new("Normal"), ThemedText)) - ), - button( + } [ + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Start, + column_gap: Val::Px(8.0), + } [ + ( + :button(ButtonProps { + on_click: callback(|_: In| { + info!("Normal button clicked!"); + }), + ..default() + }) [(Text("Normal") ThemedText)] + ), + ( + :button( ButtonProps { - on_click: Callback::System(commands.register_system( - |_: In| { - info!("Disabled button clicked!"); - } - )), + on_click: callback(|_: In| { + info!("Disabled button clicked!"); + }), ..default() }, - InteractionDisabled, - Spawn((Text::new("Disabled"), ThemedText)) - ), - button( + ) + InteractionDisabled::default() + [(Text("Disabled") ThemedText)] + ), + ( + :button( ButtonProps { - on_click: Callback::System(commands.register_system( - |_: In| { - info!("Primary button clicked!"); - } - )), + on_click: callback(|_: In| { + info!("Primary button clicked!"); + }), variant: ButtonVariant::Primary, ..default() }, - (), - Spawn((Text::new("Primary"), ThemedText)) - ), - ] - ), - ( - Node { - display: Display::Flex, - flex_direction: FlexDirection::Row, - align_items: AlignItems::Center, - justify_content: JustifyContent::Start, - column_gap: Val::Px(1.0), - ..default() - }, - children![ - button( - ButtonProps { - on_click: Callback::System(commands.register_system( - |_: In| { - info!("Left button clicked!"); - } - )), - corners: RoundedCorners::Left, - ..default() - }, - (), - Spawn((Text::new("Left"), ThemedText)) - ), - button( - ButtonProps { - on_click: Callback::System(commands.register_system( - |_: In| { - info!("Center button clicked!"); - } - )), - corners: RoundedCorners::None, - ..default() - }, - (), - Spawn((Text::new("Center"), ThemedText)) - ), - button( - ButtonProps { - on_click: Callback::System(commands.register_system( - |_: In| { - info!("Right button clicked!"); - } - )), - variant: ButtonVariant::Primary, - corners: RoundedCorners::Right, - }, - (), - Spawn((Text::new("Right"), ThemedText)) - ), - ] - ), - button( + ) [(Text("Primary") ThemedText)] + ), + ], + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Start, + column_gap: Val::Px(1.0), + } [ + ( + :button(ButtonProps { + on_click: callback(|_: In| { + info!("Left button clicked!"); + }), + corners: RoundedCorners::Left, + ..default() + }) [(Text("Left") ThemedText)] + ), + ( + :button(ButtonProps { + on_click: callback(|_: In| { + info!("Center button clicked!"); + }), + corners: RoundedCorners::None, + ..default() + }) [(Text("Center") ThemedText)] + ), + ( + :button(ButtonProps { + on_click: callback(|_: In| { + info!("Right button clicked!"); + }), + variant: ButtonVariant::Primary, + corners: RoundedCorners::Right, + }) [(Text("Right") ThemedText)] + ), + ], + :button( ButtonProps { - on_click: Callback::System(commands.register_system(|_: In| { + on_click: callback(|_: In| { info!("Wide button clicked!"); - })), + }), ..default() - }, - (), - Spawn((Text::new("Button"), ThemedText)) - ), - checkbox( - CheckboxProps { - on_change: Callback::Ignore, - }, - Checked, - Spawn((Text::new("Checkbox"), ThemedText)) + } + ) [(Text("Button") ThemedText)], + ( + :checkbox(CheckboxProps::default()) + Checked::default() + [(Text("Checkbox") ThemedText)] ), - checkbox( - CheckboxProps { - on_change: Callback::Ignore, - }, - InteractionDisabled, - Spawn((Text::new("Disabled"), ThemedText)) + ( + :checkbox(CheckboxProps::default()) + InteractionDisabled::default() + [(Text("Disabled") ThemedText)] ), - checkbox( - CheckboxProps { - on_change: Callback::Ignore, - }, - (InteractionDisabled, Checked), - Spawn((Text::new("Disabled+Checked"), ThemedText)) + ( + :checkbox(CheckboxProps::default()) + InteractionDisabled + Checked::default() + [(Text("Disabled+Checked") ThemedText)] ), ( Node { display: Display::Flex, flex_direction: FlexDirection::Column, row_gap: Val::Px(4.0), - ..default() - }, + } CoreRadioGroup { - on_change: Callback::System(radio_exclusion), - }, - 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)) - ), - ] - ), - ( - Node { - display: Display::Flex, - flex_direction: FlexDirection::Row, - align_items: AlignItems::Center, - justify_content: JustifyContent::Start, - column_gap: Val::Px(8.0), - ..default() - }, - children![ - toggle_switch( - ToggleSwitchProps { - on_change: Callback::Ignore, - }, - (), - ), - toggle_switch( - ToggleSwitchProps { - on_change: Callback::Ignore, - }, - InteractionDisabled, - ), - toggle_switch( - ToggleSwitchProps { - on_change: Callback::Ignore, + // Update radio button states based on notification from radio group. + on_change: callback( + |ent: In, q_radio: Query>, mut commands: Commands| { + for radio in q_radio.iter() { + if radio == ent.0.0 { + commands.entity(radio).insert(Checked); + } else { + commands.entity(radio).remove::(); + } + } }, - (InteractionDisabled, Checked), ), + } + [ + :radio Checked::default() [(Text("One") ThemedText)], + :radio [(Text("Two") ThemedText)], + :radio [(Text("Three") ThemedText)], + :radio InteractionDisabled::default() [(Text("Disabled") ThemedText)], ] ), - slider( - SliderProps { + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Start, + column_gap: Val::Px(8.0), + } [ + :toggle_switch(ToggleSwitchProps::default()), + :toggle_switch(ToggleSwitchProps::default()) InteractionDisabled, + :toggle_switch(ToggleSwitchProps::default()) InteractionDisabled Checked, + ], + ( + :slider(SliderProps { max: 100.0, value: 20.0, ..default() - }, - (SliderStep(10.), SliderPrecision(2)), + }) + SliderStep(10.) + SliderPrecision(2) ), + ], + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + align_items: AlignItems::Stretch, + justify_content: JustifyContent::Start, + padding: UiRect::all(Val::Px(8.0)), + row_gap: Val::Px(8.0), + width: Val::Percent(30.), + min_width: Val::Px(200.), + } [ + ( + :subpane [ + :subpane_header [ + (Text("Left") ThemedText), + (Text("Center") ThemedText), + (Text("Right") ThemedText) + ], + :subpane_body [ + (Text("Body") ThemedText), + ], + ] + ), + ( + :pane [ + :pane_header [ + :tool_button(ButtonProps { + variant: ButtonVariant::Selected, + ..default() + }) [ + (Text("\u{0398}") ThemedText) + ], + :pane_header_divider, + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{00BC}") ThemedText) + ], + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{00BD}") ThemedText) + ], + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{00BE}") ThemedText) + ], + :pane_header_divider, + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{20AC}") ThemedText) + ], + :flex_spacer, + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{00D7}") ThemedText) + ], + ], + ( + :pane_body [ + (Text("Some") ThemedText), + (Text("Content") ThemedText), + (Text("Here") ThemedText), + ] + BackgroundColor(palettes::tailwind::EMERALD_800) + ), + ] + ) ] - ),], - ) + ] + } }