diff --git a/Cargo.toml b/Cargo.toml index 261cb23632208..59f3c631f47ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,6 +151,7 @@ default = [ "bevy_picking", "bevy_render", "bevy_scene", + "bevy_scene2", "bevy_sprite", "bevy_sprite_picking_backend", "bevy_state", @@ -175,6 +176,7 @@ default = [ "wayland", "debug", "zstd_rust", + "experimental_bevy_feathers", ] # Recommended defaults for no_std applications @@ -257,6 +259,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", @@ -616,8 +621,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 } @@ -634,6 +640,7 @@ event-listener = "5.3.0" anyhow = "1" accesskit = "0.21" nonmax = "0.5" +variadics_please = "1" [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] ureq = { version = "3.0.8", features = ["json"] } @@ -2814,6 +2821,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 90facc26237c6..f4886be515c5b 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -224,7 +224,7 @@ impl ManageAccessibilityUpdates { /// /// This behavior may or may not be intended, so please utilize /// `AccessibilityNode`s with care. -#[derive(Component, Clone, Deref, DerefMut)] +#[derive(Component, Clone, Deref, DerefMut, Default)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct AccessibilityNode( /// A representation of this component's entity to `AccessKit`. diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index 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 2bcb6b136dd80..4769f5b3efcb3 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -471,6 +471,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()); @@ -485,18 +491,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 641952a67150e..b18967e601ae8 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -865,6 +865,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 d27fb21220af3..a009fa324d638 100644 --- a/crates/bevy_core_widgets/src/callback.rs +++ b/crates/bevy_core_widgets/src/callback.rs @@ -1,6 +1,8 @@ -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 bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_reflect::Reflect; +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 @@ -28,16 +30,119 @@ use bevy_reflect::{prelude::ReflectDefault, Reflect}; /// // Later, when we want to execute the callback: /// app.world_mut().commands().notify(&callback); /// ``` -#[derive(Default, Debug, Reflect)] -#[reflect(Default)] +#[derive(Debug, Reflect)] 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 70b6fb1cb355f..d9ba60921f3bf 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 6ac92e6cce3c5..29bab28fd76fb 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 4cea61e717517..4132918cc4a36 100644 --- a/crates/bevy_core_widgets/src/core_radio.rs +++ b/crates/bevy_core_widgets/src/core_radio.rs @@ -19,6 +19,7 @@ use bevy_reflect::Reflect; 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 @@ -35,7 +36,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. @@ -48,7 +49,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)] #[derive(Reflect)] #[reflect(Component)] diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index 64570c2ed1963..d55a8d4614372 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -26,6 +26,7 @@ use bevy_reflect::{prelude::ReflectDefault, Reflect}; 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, Reflect)] @@ -69,7 +70,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, @@ -88,7 +89,7 @@ pub struct CoreSlider { } /// Marker component that identifies which descendant element is the slider thumb. -#[derive(Component, Debug, Default)] +#[derive(Component, Debug, Default, Clone)] pub struct CoreSliderThumb; /// A component which stores the current value of the slider. 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 c4d685f86662e..75a0910d4b96e 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -120,6 +120,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 964bc56adbbb2..ffd4aef82c613 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 07404459c6c36..125c8473cc664 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 5307b3785e7cc..9a37baf7d4f2e 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, }, + 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 aee34f5158655..2d7682f0a75fd 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 68af828d93818..3010cecfdc962 100644 --- a/crates/bevy_feathers/src/controls/button.rs +++ b/crates/bevy_feathers/src/controls/button.rs @@ -1,31 +1,28 @@ 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}, reflect::ReflectComponent, schedule::IntoScheduleConfigs, - spawn::{SpawnRelated, SpawnableList}, system::{Commands, In, Query}, }; -use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_scene2::{prelude::*, template_value}; use bevy_ui::{AlignItems, InteractionDisabled, JustifyContent, Node, Pressed, UiRect, Val}; use crate::{ constants::{fonts, size}, cursor::EntityCursor, font_styles::InheritableFont, - handle_or_path::HandleOrPath, rounded_corners::RoundedCorners, theme::{ThemeBackgroundColor, ThemeFontColor}, tokens, }; +use bevy_input_focus::tab_navigation::TabIndex; /// Color variants for buttons. This also functions as a component used by the dynamic styling /// system to identify which entities are buttons. @@ -38,6 +35,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. @@ -48,46 +49,68 @@ 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(), - EntityCursor::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 + EntityCursor::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 + EntityCursor::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( @@ -174,11 +197,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 328098eeb2578..9cefa3e96a80f 100644 --- a/crates/bevy_feathers/src/controls/checkbox.rs +++ b/crates/bevy_feathers/src/controls/checkbox.rs @@ -1,16 +1,13 @@ 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}, reflect::ReflectComponent, schedule::IntoScheduleConfigs, - spawn::{Spawn, SpawnRelated, SpawnableList}, system::{Commands, In, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; @@ -18,6 +15,7 @@ use bevy_math::Rot2; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_render::view::Visibility; +use bevy_scene2::prelude::*; use bevy_ui::{ AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, Node, PositionType, UiRect, UiTransform, Val, @@ -27,7 +25,6 @@ use crate::{ constants::{fonts, size}, cursor::EntityCursor, font_styles::InheritableFont, - handle_or_path::HandleOrPath, theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, tokens, }; @@ -36,7 +33,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) @@ -54,74 +51,60 @@ struct CheckboxOutline; #[reflect(Component, Clone, Default)] struct CheckboxMark; -/// Template function to spawn a checkbox. +/// Checkbox scene function. /// /// # Arguments /// * `props` - construction properties for the checkbox. -/// * `overrides` - a bundle of components that are merged in with the normal checkbox components. -/// * `label` - the label of the checkbox. -pub fn checkbox + Send + Sync + 'static, B: Bundle>( - 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(), - EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - ThemeFontColor(tokens::CHECKBOX_TEXT), + on_change: {props.on_change.clone()}, + } + CheckboxFrame + Hovered + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeFontColor(tokens::CHECKBOX_TEXT) InheritableFont { - font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font: fonts::REGULAR, font_size: 14.0, - }, - overrides, - Children::spawn(( - Spawn(( + } + [( + Node { + width: size::CHECKBOX_SIZE, + height: size::CHECKBOX_SIZE, + border: UiRect::all(Val::Px(2.0)), + } + CheckboxOutline + BorderRadius::all(Val::Px(4.0)) + ThemeBackgroundColor(tokens::CHECKBOX_BG) + ThemeBorderColor(tokens::CHECKBOX_BORDER) + [( + // Cheesy checkmark: rotated node with L-shaped border. Node { - width: size::CHECKBOX_SIZE, - height: size::CHECKBOX_SIZE, - border: UiRect::all(Val::Px(2.0)), - ..Default::default() - }, - CheckboxOutline, - BorderRadius::all(Val::Px(4.0)), - ThemeBackgroundColor(tokens::CHECKBOX_BG), - ThemeBorderColor(tokens::CHECKBOX_BORDER), - children![( - // Cheesy checkmark: rotated node with L-shaped border. - Node { - position_type: PositionType::Absolute, - left: Val::Px(4.0), - top: Val::Px(0.0), - width: Val::Px(6.), - height: Val::Px(11.), - border: UiRect { - bottom: Val::Px(2.0), - right: Val::Px(2.0), - ..Default::default() - }, - ..Default::default() + position_type: PositionType::Absolute, + left: Val::Px(4.0), + top: Val::Px(0.0), + width: Val::Px(6.), + height: Val::Px(11.), + border: UiRect { + bottom: Val::Px(2.0), + right: Val::Px(2.0), }, - UiTransform::from_rotation(Rot2::FRAC_PI_4), - CheckboxMark, - ThemeBorderColor(tokens::CHECKBOX_MARK), - )], - )), - label, - )), - ) + } + UiTransform::from_rotation(Rot2::FRAC_PI_4) + CheckboxMark + ThemeBorderColor(tokens::CHECKBOX_MARK) + )] + )] + } } fn update_checkbox_styles( diff --git a/crates/bevy_feathers/src/controls/color_slider.rs b/crates/bevy_feathers/src/controls/color_slider.rs index 76ad6523ab4c0..1f70b2d4c9eec 100644 --- a/crates/bevy_feathers/src/controls/color_slider.rs +++ b/crates/bevy_feathers/src/controls/color_slider.rs @@ -4,22 +4,22 @@ use bevy_app::{Plugin, PreUpdate}; use bevy_asset::Handle; use bevy_color::{Alpha, Color, Hsla}; use bevy_core_widgets::{ - Callback, CoreSlider, CoreSliderThumb, SliderRange, SliderValue, TrackClick, ValueChange, + CallbackTemplate, CoreSlider, CoreSliderThumb, SliderRange, SliderValue, TrackClick, + ValueChange, }; use bevy_ecs::{ bundle::Bundle, - children, component::Component, entity::Entity, hierarchy::Children, query::{Changed, Or, With}, schedule::IntoScheduleConfigs, - spawn::SpawnRelated, system::{In, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_log::warn_once; use bevy_picking::PickingSystems; +use bevy_scene2::{bsn, template_value, Scene}; use bevy_ui::{ AlignItems, BackgroundColor, BackgroundGradient, BorderColor, BorderRadius, ColorStop, Display, FlexDirection, Gradient, InterpolationColorSpace, LinearGradient, Node, Outline, PositionType, @@ -147,7 +147,7 @@ pub struct ColorSliderProps { /// Slider current value pub value: f32, /// On-change handler - pub on_change: Callback>>, + pub on_change: CallbackTemplate>>, /// Which color component we're editing pub channel: ColorChannel, } @@ -156,7 +156,7 @@ impl Default for ColorSliderProps { fn default() -> Self { Self { value: 0.0, - on_change: Callback::Ignore, + on_change: CallbackTemplate::Ignore, channel: ColorChannel::Alpha, } } @@ -164,7 +164,7 @@ impl Default for ColorSliderProps { /// A color slider widget. #[derive(Component, Default, Clone)] -#[require(CoreSlider, SliderBaseColor(Color::WHITE))] +#[require(SliderBaseColor(Color::WHITE))] pub struct ColorSlider { /// Which channel is being edited by this slider. pub channel: ColorChannel, @@ -184,107 +184,99 @@ struct ColorSliderThumb; /// /// * `props` - construction properties for the slider. /// * `overrides` - a bundle of components that are merged in with the normal slider components. -pub fn color_slider(props: ColorSliderProps, overrides: B) -> impl Bundle { - ( +pub fn color_slider(props: ColorSliderProps) -> impl Scene { + let channel_range = props.channel.range(); + bsn! { Node { display: Display::Flex, flex_direction: FlexDirection::Row, height: Val::Px(SLIDER_HEIGHT), align_items: AlignItems::Stretch, flex_grow: 1.0, - ..Default::default() - }, + } CoreSlider { - on_change: props.on_change, + on_change: {props.on_change.clone()}, track_click: TrackClick::Snap, - }, + } ColorSlider { - channel: props.channel.clone(), - }, - SliderValue(props.value), - props.channel.range(), - EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - overrides, - children![ + channel: {props.channel.clone()}, + } + SliderValue({props.value}) + template_value(channel_range) + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + [ // track - ( - Node { - position_type: PositionType::Absolute, - left: Val::Px(0.), - right: Val::Px(0.), - top: Val::Px(TRACK_PADDING), - bottom: Val::Px(TRACK_PADDING), - ..Default::default() - }, - RoundedCorners::All.to_border_radius(TRACK_RADIUS), - ColorSliderTrack, - AlphaPattern, - MaterialNode::(Handle::default()), - children![ - // Left endcap - ( - Node { - width: Val::Px(THUMB_SIZE * 0.5), - ..Default::default() - }, - RoundedCorners::Left.to_border_radius(TRACK_RADIUS), - BackgroundColor(palette::X_AXIS), - ), - // Track with gradient - ( - Node { - flex_grow: 1.0, - ..Default::default() - }, - BackgroundGradient(vec![Gradient::Linear(LinearGradient { - angle: PI * 0.5, - stops: vec![ - ColorStop::new(Color::NONE, Val::Percent(0.)), - ColorStop::new(Color::NONE, Val::Percent(50.)), - ColorStop::new(Color::NONE, Val::Percent(100.)), - ], - color_space: InterpolationColorSpace::Srgba, - })]), - ZIndex(1), - children![( - Node { - position_type: PositionType::Absolute, - left: Val::Percent(0.), - top: Val::Percent(50.), - width: Val::Px(THUMB_SIZE), - height: Val::Px(THUMB_SIZE), - border: UiRect::all(Val::Px(2.0)), - ..Default::default() - }, - CoreSliderThumb, - ColorSliderThumb, - BorderRadius::MAX, - BorderColor::all(palette::WHITE), - Outline { - width: Val::Px(1.), - offset: Val::Px(0.), - color: palette::BLACK - }, - UiTransform::from_translation(Val2::new( - Val::Percent(-50.0), - Val::Percent(-50.0), - )) - )] - ), - // Right endcap - ( + Node { + position_type: PositionType::Absolute, + left: Val::Px(0.), + right: Val::Px(0.), + top: Val::Px(TRACK_PADDING), + bottom: Val::Px(TRACK_PADDING), + } + template_value(RoundedCorners::All.to_border_radius(TRACK_RADIUS)) + ColorSliderTrack + AlphaPattern + MaterialNode::(Handle::default()) + [ + // Left endcap + ( + Node { + width: Val::Px({THUMB_SIZE * 0.5}), + } + template_value(RoundedCorners::Left.to_border_radius(TRACK_RADIUS)) + BackgroundColor({palette::X_AXIS}) + ), + // Track with gradient + ( + Node { + flex_grow: 1.0, + } + BackgroundGradient({vec![Gradient::Linear(LinearGradient { + angle: PI * 0.5, + stops: vec![ + ColorStop::new(Color::NONE, Val::Percent(0.)), + ColorStop::new(Color::NONE, Val::Percent(50.)), + ColorStop::new(Color::NONE, Val::Percent(100.)), + ], + color_space: InterpolationColorSpace::Srgba, + })]}) + ZIndex(1) + [ Node { - width: Val::Px(THUMB_SIZE * 0.5), - ..Default::default() - }, - RoundedCorners::Right.to_border_radius(TRACK_RADIUS), - BackgroundColor(palette::Z_AXIS), - ), - ] - ), - ], - ) + position_type: PositionType::Absolute, + left: Val::Percent(0.), + top: Val::Percent(50.), + width: Val::Px(THUMB_SIZE), + height: Val::Px(THUMB_SIZE), + border: UiRect::all(Val::Px(2.0)), + } + CoreSliderThumb + ColorSliderThumb + BorderRadius::MAX + BorderColor::all(palette::WHITE) + Outline { + width: Val::Px(1.), + offset: Val::Px(0.), + color: palette::BLACK + } + UiTransform::from_translation(Val2::new( + Val::Percent(-50.0), + Val::Percent(-50.0), + )) + ] + ), + // Right endcap + ( + Node { + width: Val::Px({THUMB_SIZE * 0.5}), + } + template_value(RoundedCorners::Right.to_border_radius(TRACK_RADIUS)) + BackgroundColor({palette::Z_AXIS}) + ), + ] + ] + } } fn update_slider_pos( diff --git a/crates/bevy_feathers/src/controls/color_swatch.rs b/crates/bevy_feathers/src/controls/color_swatch.rs index 972f13b7878f5..813aed32c20e2 100644 --- a/crates/bevy_feathers/src/controls/color_swatch.rs +++ b/crates/bevy_feathers/src/controls/color_swatch.rs @@ -1,17 +1,10 @@ -use bevy_asset::Handle; use bevy_color::Alpha; -use bevy_ecs::{ - bundle::Bundle, children, component::Component, reflect::ReflectComponent, spawn::SpawnRelated, -}; +use bevy_ecs::{component::Component, reflect::ReflectComponent}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_scene2::{bsn, Scene}; use bevy_ui::{BackgroundColor, BorderRadius, Node, PositionType, Val}; -use bevy_ui_render::ui_material::MaterialNode; -use crate::{ - alpha_pattern::{AlphaPattern, AlphaPatternMaterial}, - constants::size, - palette, -}; +use crate::{alpha_pattern::AlphaPattern, constants::size, palette}; /// Marker identifying a color swatch. #[derive(Component, Default, Clone, Reflect)] @@ -29,30 +22,26 @@ pub struct ColorSwatchFg; /// /// # Arguments /// * `overrides` - a bundle of components that are merged in with the normal swatch components. -pub fn color_swatch(overrides: B) -> impl Bundle { - ( +pub fn color_swatch() -> impl Scene { + bsn! { Node { height: size::ROW_HEIGHT, min_width: size::ROW_HEIGHT, - ..Default::default() - }, - ColorSwatch, - AlphaPattern, - MaterialNode::(Handle::default()), - BorderRadius::all(Val::Px(5.0)), - overrides, - children![( + } + ColorSwatch + AlphaPattern + BorderRadius::all(Val::Px(5.0)) + [ Node { position_type: PositionType::Absolute, left: Val::Px(0.), top: Val::Px(0.), bottom: Val::Px(0.), right: Val::Px(0.), - ..Default::default() - }, - ColorSwatchFg, - BackgroundColor(palette::ACCENT.with_alpha(0.5)), + } + ColorSwatchFg + BackgroundColor({palette::ACCENT.with_alpha(0.5)}) BorderRadius::all(Val::Px(5.0)) - ),], - ) + ] + } } diff --git a/crates/bevy_feathers/src/controls/mod.rs b/crates/bevy_feathers/src/controls/mod.rs index f5b9ef4c43dd5..c4449d6a1b0a6 100644 --- a/crates/bevy_feathers/src/controls/mod.rs +++ b/crates/bevy_feathers/src/controls/mod.rs @@ -10,7 +10,7 @@ mod slider; mod toggle_switch; mod virtual_keyboard; -pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant}; +pub use button::{button, tool_button, ButtonPlugin, ButtonProps, ButtonVariant}; pub use checkbox::{checkbox, CheckboxPlugin, CheckboxProps}; pub use color_slider::{ color_slider, ColorChannel, ColorSlider, ColorSliderPlugin, ColorSliderProps, SliderBaseColor, @@ -21,15 +21,12 @@ pub use slider::{slider, SliderPlugin, SliderProps}; pub use toggle_switch::{toggle_switch, ToggleSwitchPlugin, ToggleSwitchProps}; pub use virtual_keyboard::virtual_keyboard; -use crate::alpha_pattern::AlphaPatternPlugin; - /// Plugin which registers all `bevy_feathers` controls. pub struct ControlsPlugin; impl Plugin for ControlsPlugin { fn build(&self, app: &mut bevy_app::App) { app.add_plugins(( - AlphaPatternPlugin, ButtonPlugin, CheckboxPlugin, ColorSliderPlugin, diff --git a/crates/bevy_feathers/src/controls/radio.rs b/crates/bevy_feathers/src/controls/radio.rs index 0b13069158919..3049bbb3945f9 100644 --- a/crates/bevy_feathers/src/controls/radio.rs +++ b/crates/bevy_feathers/src/controls/radio.rs @@ -1,22 +1,20 @@ 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}, reflect::ReflectComponent, schedule::IntoScheduleConfigs, - spawn::{Spawn, SpawnRelated, SpawnableList}, system::{Commands, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_render::view::Visibility; +use bevy_scene2::prelude::*; use bevy_ui::{ AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, Node, UiRect, Val, @@ -26,7 +24,6 @@ use crate::{ constants::{fonts, size}, cursor::EntityCursor, font_styles::InheritableFont, - handle_or_path::HandleOrPath, theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, tokens, }; @@ -41,64 +38,49 @@ struct RadioOutline; #[reflect(Component, Clone, Default)] struct RadioMark; -/// Template function to spawn a radio. -/// -/// # Arguments -/// * `props` - construction properties for the radio. -/// * `overrides` - a bundle of components that are merged in with the normal radio components. -/// * `label` - the label of the radio. -pub fn radio + Send + Sync + 'static, B: Bundle>( - overrides: B, - label: C, -) -> impl Bundle { - ( +/// Radio scene function. +pub fn radio() -> impl Scene { + bsn! { Node { display: Display::Flex, flex_direction: FlexDirection::Row, justify_content: JustifyContent::Start, align_items: AlignItems::Center, column_gap: Val::Px(4.0), - ..Default::default() - }, - CoreRadio, - Hovered::default(), - EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - ThemeFontColor(tokens::RADIO_TEXT), + } + CoreRadio + Hovered + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeFontColor(tokens::RADIO_TEXT) InheritableFont { - font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font: fonts::REGULAR, font_size: 14.0, - }, - overrides, - Children::spawn(( - Spawn(( + } + [( + Node { + display: Display::Flex, + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + width: size::RADIO_SIZE, + height: size::RADIO_SIZE, + border: UiRect::all(Val::Px(2.0)), + } + RadioOutline + BorderRadius::MAX + ThemeBorderColor(tokens::RADIO_BORDER) + [( + // Cheesy checkmark: rotated node with L-shaped border. Node { - display: Display::Flex, - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - width: size::RADIO_SIZE, - height: size::RADIO_SIZE, - border: UiRect::all(Val::Px(2.0)), - ..Default::default() - }, - RadioOutline, - BorderRadius::MAX, - ThemeBorderColor(tokens::RADIO_BORDER), - children![( - // Cheesy checkmark: rotated node with L-shaped border. - Node { - width: Val::Px(8.), - height: Val::Px(8.), - ..Default::default() - }, - BorderRadius::MAX, - RadioMark, - ThemeBackgroundColor(tokens::RADIO_MARK), - )], - )), - label, - )), - ) + width: Val::Px(8.), + height: Val::Px(8.), + } + BorderRadius::MAX + RadioMark + ThemeBackgroundColor(tokens::RADIO_MARK) + )] + )] + } } fn update_radio_styles( diff --git a/crates/bevy_feathers/src/controls/slider.rs b/crates/bevy_feathers/src/controls/slider.rs index fcea771806cd9..8855fe077f873 100644 --- a/crates/bevy_feathers/src/controls/slider.rs +++ b/crates/bevy_feathers/src/controls/slider.rs @@ -2,10 +2,10 @@ 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, @@ -13,12 +13,12 @@ use bevy_ecs::{ query::{Added, Changed, Has, Or, Spawned, With}, reflect::ReflectComponent, schedule::IntoScheduleConfigs, - spawn::SpawnRelated, system::{In, Query, Res}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::PickingSystems; use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_scene2::{prelude::*, template_value}; use bevy_ui::{ widget::Text, AlignItems, BackgroundGradient, ColorStop, Display, FlexDirection, Gradient, InteractionDisabled, InterpolationColorSpace, JustifyContent, LinearGradient, Node, UiRect, @@ -29,7 +29,6 @@ use crate::{ constants::{fonts, size}, cursor::EntityCursor, font_styles::InheritableFont, - handle_or_path::HandleOrPath, rounded_corners::RoundedCorners, theme::{ThemeFontColor, ThemedText, UiTheme}, tokens, @@ -44,7 +43,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 { @@ -53,14 +52,12 @@ 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)] -#[derive(Reflect)] +#[derive(Component, Default, Clone, Reflect)] #[reflect(Component, Clone, Default)] struct SliderStyle; @@ -69,34 +66,32 @@ struct SliderStyle; #[reflect(Component, Clone, Default)] struct SliderValueText; -/// Spawn a new slider widget. +/// Slider scene function. /// /// # Arguments /// /// * `props` - construction properties for the slider. -/// * `overrides` - a bundle of components that are merged in with the normal slider components. -pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { - ( +pub fn slider(props: SliderProps) -> impl Scene { + bsn! { Node { height: size::ROW_HEIGHT, justify_content: JustifyContent::Center, align_items: AlignItems::Center, padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)), flex_grow: 1.0, - ..Default::default() - }, + } 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), - EntityCursor::System(bevy_window::SystemCursorIcon::EwResize), - TabIndex(0), - RoundedCorners::All.to_border_radius(6.0), + } + SliderStyle + SliderValue({props.value}) + SliderRange::new(props.min, props.max) + EntityCursor::System(bevy_window::SystemCursorIcon::EwResize) + TabIndex(0) + template_value(RoundedCorners::All.to_border_radius(6.0)) // Use a gradient to draw the moving bar - BackgroundGradient(vec![Gradient::Linear(LinearGradient { + BackgroundGradient({vec![Gradient::Linear(LinearGradient { angle: PI * 0.5, stops: vec![ ColorStop::new(Color::NONE, Val::Percent(0.)), @@ -105,25 +100,23 @@ pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { ColorStop::new(Color::NONE, Val::Percent(100.)), ], color_space: InterpolationColorSpace::Srgba, - })]), - overrides, - children![( + })]}) + [( // Text container Node { display: Display::Flex, 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 46872d97b0e8b..673528bef75a6 100644 --- a/crates/bevy_feathers/src/controls/toggle_switch.rs +++ b/crates/bevy_feathers/src/controls/toggle_switch.rs @@ -1,10 +1,8 @@ use accesskit::Role; use bevy_a11y::AccessibilityNode; 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::Children, @@ -12,13 +10,13 @@ use bevy_ecs::{ query::{Added, Changed, Has, Or, With}, reflect::ReflectComponent, 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_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_scene2::prelude::*; use bevy_ui::{BorderRadius, Checked, InteractionDisabled, Node, PositionType, UiRect, Val}; use crate::{ @@ -32,7 +30,7 @@ use crate::{ #[derive(Default)] pub struct ToggleSwitchProps { /// Change handler - pub on_change: Callback>>, + pub on_change: CallbackTemplate>>, } /// Marker for the toggle switch outline @@ -45,45 +43,41 @@ struct ToggleSwitchOutline; #[reflect(Component, Clone, Default)] struct ToggleSwitchSlide; -/// Template function to spawn a toggle switch. +/// Toggle switch scene function. /// /// # Arguments /// * `props` - construction properties for the toggle switch. -/// * `overrides` - a bundle of components that are merged in with the normal toggle switch components. -pub fn toggle_switch(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(), - EntityCursor::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 + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + [( Node { position_type: PositionType::Absolute, left: Val::Percent(0.), top: Val::Px(0.), bottom: Val::Px(0.), width: Val::Percent(50.), - ..Default::default() - }, - BorderRadius::all(Val::Px(3.0)), - ToggleSwitchSlide, - ThemeBackgroundColor(tokens::SWITCH_SLIDE), - )], - ) + } + BorderRadius::all(Val::Px(3.0)) + ToggleSwitchSlide + ThemeBackgroundColor(tokens::SWITCH_SLIDE) + )] + } } fn update_switch_styles( diff --git a/crates/bevy_feathers/src/controls/virtual_keyboard.rs b/crates/bevy_feathers/src/controls/virtual_keyboard.rs index 6f0473ffe0611..b4981aa44bd2a 100644 --- a/crates/bevy_feathers/src/controls/virtual_keyboard.rs +++ b/crates/bevy_feathers/src/controls/virtual_keyboard.rs @@ -1,13 +1,11 @@ -use bevy_core_widgets::{Activate, Callback}; +use bevy_core_widgets::{Activate, CallbackTemplate}; use bevy_ecs::{ - bundle::Bundle, component::Component, - hierarchy::{ChildOf, Children}, - relationship::RelatedSpawner, - spawn::{Spawn, SpawnRelated, SpawnWith}, system::{In, SystemId}, + template::template, }; use bevy_input_focus::tab_navigation::TabGroup; +use bevy_scene2::prelude::*; use bevy_ui::Node; use bevy_ui::Val; use bevy_ui::{widget::Text, FlexDirection}; @@ -15,42 +13,33 @@ use bevy_ui::{widget::Text, FlexDirection}; use crate::controls::{button, ButtonProps}; /// Function to spawn a virtual keyboard -pub fn virtual_keyboard( +pub fn virtual_keyboard( keys: impl Iterator> + Send + Sync + 'static, on_key_press: SystemId>, -) -> impl Bundle -where - T: Component, -{ - ( +) -> impl Scene { + let children: Vec<_> = keys.map(|row| { + let children: Vec<_> = row.into_iter().map(|(label, key_id)| { + bsn! { + :button(ButtonProps { on_click: CallbackTemplate::SystemId(on_key_press), ..Default::default() }) + template(move |entity| {entity.insert(key_id.clone()); Ok(())}) + [Text::new(label.clone())] + } + }).collect(); + + bsn! { + Node { + flex_direction: FlexDirection::Row, + column_gap: Val::Px(4.), + } + [{ children }] + } + }).collect(); + bsn! { Node { flex_direction: FlexDirection::Column, row_gap: Val::Px(4.), - ..Default::default() - }, - TabGroup::new(0), - Children::spawn((SpawnWith(move |parent: &mut RelatedSpawner| { - for row in keys { - parent.spawn(( - Node { - flex_direction: FlexDirection::Row, - column_gap: Val::Px(4.), - ..Default::default() - }, - Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner| { - for (label, key_id) in row.into_iter() { - parent.spawn(button( - ButtonProps { - on_click: Callback::System(on_key_press), - ..Default::default() - }, - (key_id,), - Spawn(Text::new(label)), - )); - } - })), - )); - } - }),)), - ) + } + TabGroup::new(0) + [ {children} ] + } } diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs index 8c6b5c103b20a..79b8487cf9a9a 100644 --- a/crates/bevy_feathers/src/cursor.rs +++ b/crates/bevy_feathers/src/cursor.rs @@ -9,6 +9,7 @@ use bevy_ecs::{ resource::Resource, schedule::IntoScheduleConfigs, system::{Commands, Query, Res}, + VariantDefaults, }; use bevy_picking::{hover::HoverMap, pointer::PointerId, PickingSystems}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -27,7 +28,7 @@ pub struct DefaultCursor(pub EntityCursor); /// /// This is effectively the same type as [`CustomCursor`] but with different methods, and used /// in different places. -#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)] +#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq, VariantDefaults)] #[reflect(Component, Debug, Default, PartialEq, Clone)] pub enum EntityCursor { #[cfg(feature = "custom_cursor")] diff --git a/crates/bevy_feathers/src/dark_theme.rs b/crates/bevy_feathers/src/dark_theme.rs index 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 6419696bb70f4..fa808097a43ec 100644 --- a/crates/bevy_feathers/src/font_styles.rs +++ b/crates/bevy_feathers/src/font_styles.rs @@ -1,64 +1,39 @@ //! 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, reflect::ReflectComponent, - system::{Commands, Query, Res}, + system::{Commands, Query}, + template::GetTemplate, }; -use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_reflect::Reflect; use bevy_text::{Font, TextFont}; -use crate::handle_or_path::HandleOrPath; - /// 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, Reflect)] -#[reflect(Component, Default)] +#[derive(Component, Clone, Debug, Reflect, GetTemplate)] +#[reflect(Component)] 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()) - && let Some(font) = match style.font { - HandleOrPath::Handle(ref h) => Some(h.clone()), - HandleOrPath::Path(ref p) => Some(assets.load::(p)), - } - { + if let Ok(inheritable_font) = font_style.get(ev.target()) { commands.entity(ev.target()).insert(Propagate(TextFont { - font, - font_size: style.font_size, + font: inheritable_font.font.clone(), + font_size: inheritable_font.font_size, ..Default::default() })); } diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs index ef70be1844ed0..17a9c27ca0b19 100644 --- a/crates/bevy_feathers/src/lib.rs +++ b/crates/bevy_feathers/src/lib.rs @@ -25,7 +25,7 @@ use bevy_text::{TextColor, TextFont}; use bevy_ui_render::UiMaterialPlugin; use crate::{ - alpha_pattern::{AlphaPatternMaterial, AlphaPatternResource}, + alpha_pattern::AlphaPatternMaterial, controls::ControlsPlugin, cursor::{CursorIconPlugin, DefaultCursor, EntityCursor}, theme::{ThemedText, UiTheme}, @@ -33,6 +33,7 @@ use crate::{ mod alpha_pattern; pub mod constants; +pub mod containers; pub mod controls; pub mod cursor; pub mod dark_theme; @@ -77,7 +78,5 @@ impl Plugin for FeathersPlugin { .add_observer(theme::on_changed_border) .add_observer(theme::on_changed_font_color) .add_observer(font_styles::on_changed_font); - - app.init_resource::(); } } diff --git a/crates/bevy_feathers/src/theme.rs b/crates/bevy_feathers/src/theme.rs index 1bb8f08e61825..78048a471028b 100644 --- a/crates/bevy_feathers/src/theme.rs +++ b/crates/bevy_feathers/src/theme.rs @@ -53,7 +53,7 @@ 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)] #[derive(Reflect)] @@ -62,7 +62,7 @@ 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)] #[derive(Reflect)] @@ -70,7 +70,7 @@ pub struct ThemeBackgroundColor(pub &'static str); 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)] #[derive(Reflect)] #[reflect(Component, Clone)] @@ -78,7 +78,7 @@ 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, Reflect)] +#[derive(Component, Reflect, Default, Clone)] #[reflect(Component)] pub struct ThemedText; diff --git a/crates/bevy_feathers/src/tokens.rs b/crates/bevy_feathers/src/tokens.rs index 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 b7a7a715a360c..fcd08bd6e372a 100644 --- a/crates/bevy_image/src/texture_atlas.rs +++ b/crates/bevy_image/src/texture_atlas.rs @@ -203,7 +203,7 @@ impl TextureAtlasLayout { /// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs) /// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs) /// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs) -#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -216,6 +216,15 @@ pub struct TextureAtlas { pub index: usize, } +impl Default for TextureAtlas { + fn default() -> Self { + Self { + layout: Handle::default(), + index: Default::default(), + } + } +} + impl TextureAtlas { /// Retrieves the current texture [`URect`] of the sprite sheet according to the section `index` pub fn texture_rect(&self, texture_atlases: &Assets) -> Option { diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index c662a4858dad8..8f10302ca1869 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -458,6 +458,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 67b1e465e715f..28188c93e0f18 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -72,6 +72,8 @@ pub use bevy_remote as remote; pub use bevy_render as render; #[cfg(feature = "bevy_scene")] pub use bevy_scene as scene; +#[cfg(feature = "bevy_scene2")] +pub use bevy_scene2 as scene2; #[cfg(feature = "bevy_shader")] pub use bevy_shader as shader; #[cfg(feature = "bevy_solari")] diff --git a/crates/bevy_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 eb3a65d0bef32..fb531aac8fcb2 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -455,10 +455,16 @@ pub struct RenderWireframeMaterial { pub color: [f32; 4], } -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq)] #[reflect(Component, Default, Clone, PartialEq)] pub struct 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 c2cba3ad3e408..abaf79856c184 100644 --- a/crates/bevy_render/src/render_resource/pipeline.rs +++ b/crates/bevy_render/src/render_resource/pipeline.rs @@ -114,7 +114,7 @@ pub struct RenderPipelineDescriptor { pub zero_initialize_workgroup_memory: bool, } -#[derive(Copy, Clone, Debug, Error)] +#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)] #[error("RenderPipelineDescriptor has no FragmentState configured")] pub struct NoFragmentStateError; @@ -128,7 +128,7 @@ impl RenderPipelineDescriptor { } } -#[derive(Clone, Debug, Eq, PartialEq, Default)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct VertexState { /// The compiled shader module for this stage. pub shader: Handle, @@ -140,8 +140,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, @@ -153,6 +164,17 @@ pub struct FragmentState { pub targets: Vec>, } +impl Default for FragmentState { + fn default() -> Self { + Self { + shader: Handle::default(), + shader_defs: Default::default(), + entry_point: Default::default(), + targets: Default::default(), + } + } +} + impl FragmentState { pub fn set_target(&mut self, index: usize, target: ColorTargetState) { filling_set_at(&mut self.targets, index, None, Some(target)); @@ -160,7 +182,7 @@ impl FragmentState { } /// Describes a compute pipeline. -#[derive(Clone, Debug, PartialEq, Eq, Default)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ComputePipelineDescriptor { pub label: Option>, pub layout: Vec, @@ -176,6 +198,20 @@ pub struct ComputePipelineDescriptor { pub zero_initialize_workgroup_memory: bool, } +impl Default for ComputePipelineDescriptor { + fn default() -> Self { + Self { + shader: Handle::default(), + label: Default::default(), + layout: Default::default(), + push_constant_ranges: Default::default(), + shader_defs: Default::default(), + entry_point: Default::default(), + zero_initialize_workgroup_memory: Default::default(), + } + } +} + // utility function to set a value at the specified index, extending with // a filler value if the index is out of bounds. fn filling_set_at(vec: &mut Vec, index: usize, filler: T, value: T) { diff --git a/crates/bevy_scene/src/components.rs b/crates/bevy_scene/src/components.rs index 7d4e9aef2dbf9..e1e6c93430737 100644 --- a/crates/bevy_scene/src/components.rs +++ b/crates/bevy_scene/src/components.rs @@ -11,16 +11,28 @@ use crate::{DynamicScene, Scene}; /// Adding this component will spawn the scene as a child of that entity. /// Once it's spawned, the entity will have a [`SceneInstance`](crate::SceneInstance) component. -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Debug, PartialEq, Clone)] #[require(Transform)] #[require(Visibility)] pub struct SceneRoot(pub Handle); +impl Default for SceneRoot { + fn default() -> Self { + Self(Handle::default()) + } +} + /// Adding this component will spawn the scene as a child of that entity. /// Once it's spawned, the entity will have a [`SceneInstance`](crate::SceneInstance) component. -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Debug, PartialEq, Clone)] #[require(Transform)] #[require(Visibility)] pub struct DynamicSceneRoot(pub Handle); + +impl Default for DynamicSceneRoot { + fn default() -> Self { + Self(Handle::default()) + } +} diff --git a/crates/bevy_scene2/Cargo.toml b/crates/bevy_scene2/Cargo.toml new file mode 100644 index 0000000000000..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..59d001a4c7cd2 --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/codegen.rs @@ -0,0 +1,413 @@ +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, + }) => { + // NOTE: The odd turbofish line break below avoids breaking rustfmt + 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); + // NOTE: The odd turbofish line breaks below avoid breaking rustfmt + quote! { + #bevy_scene::RelatedScenes::< + <#relationship_path as #bevy_ecs::relationship::RelationshipTarget> + ::Relationship, + _ + >::new( + #scenes + ) + } + } + BsnEntry::InheritedScene(inherited_scene) => match inherited_scene { + BsnInheritedScene::Asset(lit_str) => { + quote! {#bevy_scene::InheritSceneAsset::from(#lit_str)} + } + BsnInheritedScene::Fn { function, args } => { + quote! {#bevy_scene::InheritScene(#function(#args))} + } + }, + BsnEntry::Name(ident) => { + let name = ident.to_string(); + quote! { + <#bevy_ecs::name::Name as PatchGetTemplate>::patch( + move |value| { + *value = Name(#name.into()); + } + ) + } + } + BsnEntry::NameExpression(expr_tokens) => { + quote! { + <#bevy_ecs::name::Name as PatchGetTemplate>::patch( + move |value| { + *value = Name({#expr_tokens}.into()); + } + ) + } + } + }); + } + + quote! {(#(#entries,)*)} + } +} + +macro_rules! field_value_type { + () => { + BsnValue::Expr(_) + | BsnValue::Closure(_) + | BsnValue::Ident(_) + | BsnValue::Lit(_) + | BsnValue::Tuple(_) + }; +} + +impl BsnType { + fn to_patch_tokens( + &self, + bevy_ecs: &Path, + bevy_scene: &Path, + assignments: &mut Vec, + is_root_template: bool, + field_path: &[Member], + is_path_ref: bool, + ) { + 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..f0eafb15fbb1a --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/parse.rs @@ -0,0 +1,457 @@ +use crate::bsn::types::{ + Bsn, BsnConstructor, BsnEntry, BsnFields, BsnInheritedScene, BsnNamedField, + BsnRelatedSceneList, BsnRoot, BsnSceneList, BsnSceneListItem, BsnSceneListItems, BsnTuple, + BsnType, BsnUnnamedField, BsnValue, +}; +use proc_macro2::{Delimiter, TokenStream, TokenTree}; +use quote::quote; +use syn::{ + braced, bracketed, + buffer::Cursor, + parenthesized, + parse::{Parse, ParseBuffer, ParseStream}, + spanned::Spanned, + token::{At, Brace, Bracket, Colon, Comma, Paren}, + Block, Expr, Ident, Lit, LitStr, Path, Result, Token, +}; + +/// Functionally identical to [`Punctuated`](syn::punctuated::Punctuated), but fills the given `$list` Vec instead +/// of allocating a new one inside [`Punctuated`](syn::punctuated::Punctuated). This exists to avoid allocating an intermediate Vec. +macro_rules! parse_punctuated_vec { + ($list:ident, $input:ident, $parse:ident, $separator:ident) => { + loop { + if $input.is_empty() { + break; + } + let value = $input.parse::<$parse>()?; + $list.push(value); + if $input.is_empty() { + break; + } + $input.parse::<$separator>()?; + } + }; +} + +impl Parse for BsnRoot { + fn parse(input: ParseStream) -> Result { + Ok(BsnRoot(input.parse::>()?)) + } +} + +impl Parse for Bsn { + fn parse(input: ParseStream) -> Result { + let mut entries = Vec::new(); + if input.peek(Paren) { + let content; + parenthesized![content in input]; + while !content.is_empty() { + entries.push(content.parse::()?); + } + } else { + if ALLOW_FLAT { + while !input.is_empty() { + entries.push(input.parse::()?); + if input.peek(Comma) { + // Not ideal, but this anticipatory break allows us to parse non-parenthesized + // flat Bsn entries in SceneLists + break; + } + } + } else { + entries.push(input.parse::()?); + } + } + + Ok(Self { entries }) + } +} + +impl Parse for BsnEntry { + fn parse(input: ParseStream) -> Result { + Ok(if input.peek(Token![:]) { + BsnEntry::InheritedScene(input.parse::()?) + } else if input.peek(Token![#]) { + input.parse::()?; + if input.peek(Brace) { + BsnEntry::NameExpression(braced_tokens(input)?) + } else { + BsnEntry::Name(input.parse::()?) + } + } else if input.peek(Brace) { + BsnEntry::SceneExpression(braced_tokens(input)?) + } else if input.peek(Bracket) { + BsnEntry::ChildrenSceneList(input.parse::()?) + } else { + let is_template = input.peek(At); + if is_template { + input.parse::()?; + } + let mut path = input.parse::()?; + let path_type = PathType::new(&path); + match path_type { + PathType::Type | PathType::Enum => { + let enum_variant = if matches!(path_type, PathType::Enum) { + take_last_path_ident(&mut path) + } else { + None + }; + if input.peek(Bracket) { + // TODO: fail if this is an enum variant + BsnEntry::RelatedSceneList(BsnRelatedSceneList { + relationship_path: path, + scene_list: input.parse::()?, + }) + } else { + let fields = input.parse::()?; + let bsn_type = BsnType { + path, + enum_variant, + fields, + }; + if is_template { + BsnEntry::TemplatePatch(bsn_type) + } else { + BsnEntry::GetTemplatePatch(bsn_type) + } + } + } + PathType::TypeConst => { + let const_ident = take_last_path_ident(&mut path).unwrap(); + BsnEntry::TemplateConst { + type_path: path, + const_ident, + } + } + PathType::Const => { + todo!("A floating type-unknown const should be assumed to be a const scene right?") + } + PathType::TypeFunction => { + let function = take_last_path_ident(&mut path).unwrap(); + let args = if input.peek(Paren) { + let content; + parenthesized!(content in input); + Some(content.parse_terminated(Expr::parse, Token![,])?) + } else { + None + }; + + let bsn_constructor = BsnConstructor { + type_path: path, + function, + args, + }; + if is_template { + BsnEntry::TemplateConstructor(bsn_constructor) + } else { + BsnEntry::GetTemplateConstructor(bsn_constructor) + } + } + PathType::Function => { + if input.peek(Paren) { + let tokens = parenthesized_tokens(input)?; + BsnEntry::SceneExpression(quote! {#path(#tokens)}) + } else { + BsnEntry::SceneExpression(quote! {#path}) + } + } + } + }) + } +} + +impl Parse for BsnSceneList { + fn parse(input: ParseStream) -> Result { + let content; + bracketed!(content in input); + Ok(BsnSceneList(content.parse::()?)) + } +} + +impl Parse for BsnSceneListItems { + fn parse(input: ParseStream) -> Result { + let mut scenes = Vec::new(); + parse_punctuated_vec!(scenes, input, BsnSceneListItem, Comma); + Ok(BsnSceneListItems(scenes)) + } +} + +impl Parse for BsnSceneListItem { + fn parse(input: ParseStream) -> Result { + Ok(if input.peek(Brace) { + 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..6a64b3ad129a9 --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/types.rs @@ -0,0 +1,100 @@ +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), + NameExpression(TokenStream), + GetTemplatePatch(BsnType), + TemplatePatch(BsnType), + GetTemplateConstructor(BsnConstructor), + TemplateConstructor(BsnConstructor), + TemplateConst { type_path: Path, const_ident: Ident }, + SceneExpression(TokenStream), + InheritedScene(BsnInheritedScene), + RelatedSceneList(BsnRelatedSceneList), + ChildrenSceneList(BsnSceneList), +} + +#[derive(Debug)] +pub struct BsnType { + pub path: Path, + pub enum_variant: Option, + pub fields: BsnFields, +} + +#[derive(Debug)] +pub struct BsnRelatedSceneList { + pub relationship_path: Path, + pub scene_list: BsnSceneList, +} + +#[derive(Debug)] +pub struct BsnSceneList(pub BsnSceneListItems); + +#[derive(Debug)] +pub struct BsnSceneListItems(pub Vec); + +#[derive(Debug)] +pub enum BsnSceneListItem { + Scene(Bsn), + Expression(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..5116eca6dcfff --- /dev/null +++ b/crates/bevy_scene2/src/lib.rs @@ -0,0 +1,99 @@ +#![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_resource::() + .init_asset::() + .add_systems(Update, (resolve_scene_patches, spawn_queued).chain()) + .add_observer(on_add_scene_patch_instance); + } +} + +/// This is used by the [`bsn!`] macro to generate compile-time only references to symbols. Currently this is used +/// to add IDE support for nested type names, as it allows us to pass the input Ident from the input to the output code. +pub const fn touch_type() {} + +pub trait LoadScene { + fn load_scene<'a>( + &self, + path: impl Into>, + scene: impl Scene, + ) -> Handle; +} + +impl LoadScene for AssetServer { + fn load_scene<'a>( + &self, + path: impl Into>, + scene: impl Scene, + ) -> Handle { + let scene = ScenePatch::load(self, scene); + self.load_with_path(path, scene) + } +} + +pub struct OnTemplate(pub I, pub PhantomData (E, B, M)>); + +impl + Clone, E: EntityEvent, B: Bundle, M: 'static> Template + for OnTemplate +{ + type Output = (); + + fn build(&mut self, 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..82e16320cb38b --- /dev/null +++ b/crates/bevy_scene2/src/spawn.rs @@ -0,0 +1,120 @@ +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>, +} + +#[derive(Resource, Default)] +pub struct NewScenes { + entities: Vec, +} + +pub fn on_add_scene_patch_instance( + trigger: On, + mut new_scenes: ResMut, +) { + new_scenes.entities.push(trigger.target()); +} + +pub fn spawn_queued( + world: &mut World, + handles: &mut QueryState<&ScenePatchInstance>, + mut reader: Local>>, +) { + world.resource_scope(|world, mut patches: Mut>| { + world.resource_scope(|world, mut queued: Mut| { + world.resource_scope(|world, events: Mut>>| { + loop { + let mut new_scenes = world.resource_mut::(); + if new_scenes.entities.is_empty() { + break; + } + for entity in core::mem::take(&mut new_scenes.entities) { + if let Ok(id) = handles.get(world, entity).map(|h| h.id()) { + 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()) + && let Some(entities) = queued.waiting_entities.remove(id) + { + for entity in entities { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + 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 cf3493c87c3bb..b5b96c72bdc41 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -456,10 +456,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 c4fb8468045cd..06e2ff449ca02 100644 --- a/crates/bevy_sprite/src/tilemap_chunk/mod.rs +++ b/crates/bevy_sprite/src/tilemap_chunk/mod.rs @@ -43,7 +43,7 @@ pub struct TilemapChunkMeshCache(HashMap>); /// A component representing a chunk of a tilemap. /// Each chunk is a rectangular section of tiles that is rendered as a single mesh. -#[derive(Component, Clone, Debug, Default, Reflect)] +#[derive(Component, Clone, Debug, Reflect)] #[reflect(Component, Clone, Debug, Default)] #[component(immutable, on_insert = on_insert_tilemap_chunk)] pub struct TilemapChunk { @@ -58,6 +58,16 @@ 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(), + } + } +} /// Data for a single tile in the tilemap chunk. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Clone, Debug, Default)] diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index d4c144983aa55..a9cd4401d3c03 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -363,7 +363,7 @@ impl From for TextFont { impl Default for TextFont { fn default() -> Self { Self { - font: Default::default(), + font: Handle::default(), font_size: 20.0, line_height: LineHeight::default(), font_smoothing: Default::default(), diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs index 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_window/src/cursor/custom_cursor.rs b/crates/bevy_window/src/cursor/custom_cursor.rs index 164559baa4279..5309121082959 100644 --- a/crates/bevy_window/src/cursor/custom_cursor.rs +++ b/crates/bevy_window/src/cursor/custom_cursor.rs @@ -6,7 +6,7 @@ use bevy_math::URect; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; /// 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 @@ -40,6 +40,19 @@ pub struct CustomCursorImage { pub hotspot: (u16, u16), } +impl Default for CustomCursorImage { + fn default() -> Self { + Self { + handle: Handle::default(), + texture_atlas: Default::default(), + flip_x: Default::default(), + flip_y: Default::default(), + rect: Default::default(), + hotspot: Default::default(), + } + } +} + /// A custom cursor created from a URL. Note that this currently only works on the web. #[derive(Debug, Clone, Default, Reflect, PartialEq, Eq, Hash)] #[reflect(Debug, Default, Hash, PartialEq, Clone)] 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 c2dabdd31540c..3a53c9614485c 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 1fa6806cae56c..3f4c2daf356c1 100644 --- a/examples/ui/feathers.rs +++ b/examples/ui/feathers.rs @@ -3,14 +3,18 @@ use bevy::{ color::palettes, core_widgets::{ - Activate, Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision, + callback, Activate, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision, SliderStep, SliderValue, ValueChange, }, feathers::{ + containers::{ + flex_spacer, pane, pane_body, pane_header, pane_header_divider, subpane, subpane_body, + subpane_header, + }, controls::{ button, checkbox, color_slider, color_swatch, radio, slider, toggle_switch, - ButtonProps, ButtonVariant, CheckboxProps, ColorChannel, ColorSlider, ColorSliderProps, - ColorSwatch, SliderBaseColor, SliderProps, ToggleSwitchProps, + tool_button, ButtonProps, ButtonVariant, CheckboxProps, ColorChannel, ColorSlider, + ColorSliderProps, ColorSwatch, SliderBaseColor, SliderProps, ToggleSwitchProps, }, dark_theme::create_dark_theme, rounded_corners::RoundedCorners, @@ -22,9 +26,11 @@ use bevy::{ InputDispatchPlugin, }, prelude::*, + scene2::prelude::{Scene, *}, ui::{Checked, InteractionDisabled}, winit::WinitSettings, }; +use bevy_ecs::VariantDefaults; /// A struct to hold the state of various widgets shown in the demo. #[derive(Resource)] @@ -33,8 +39,9 @@ struct DemoWidgetStates { hsl_color: Hsla, } -#[derive(Component, Clone, Copy, PartialEq)] +#[derive(Component, Clone, Copy, PartialEq, Default, VariantDefaults)] enum SwatchType { + #[default] Rgb, Hsl, } @@ -63,80 +70,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::(); - } - } - }, - ); - - let change_red = commands.register_system( - |change: In>, mut color: ResMut| { - color.rgb_color.red = change.value; - }, - ); - - let change_green = commands.register_system( - |change: In>, mut color: ResMut| { - color.rgb_color.green = change.value; - }, - ); - - let change_blue = commands.register_system( - |change: In>, mut color: ResMut| { - color.rgb_color.blue = change.value; - }, - ); - - let change_alpha = commands.register_system( - |change: In>, mut color: ResMut| { - color.rgb_color.alpha = change.value; - }, - ); - - let change_hue = commands.register_system( - |change: In>, mut color: ResMut| { - color.hsl_color.hue = change.value; - }, - ); - - let change_saturation = commands.register_system( - |change: In>, mut color: ResMut| { - color.hsl_color.saturation = change.value; - }, - ); - - let change_lightness = commands.register_system( - |change: In>, mut color: ResMut| { - color.hsl_color.lightness = change.value; - }, - ); - - ( +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, @@ -146,284 +96,335 @@ 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 { - max: 100.0, - value: 20.0, - ..default() - }, - (SliderStep(10.), SliderPrecision(2)), - ), + 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, + ], + Node { + flex_direction: FlexDirection::Column, + } [ + ( + :slider(SliderProps { + max: 100.0, + value: 20.0, + ..default() + }) + SliderStep(10.) + SliderPrecision(2) + ), + ( + Node { + justify_content: JustifyContent::SpaceBetween, + } + [Text("Srgba"), (color_swatch() SwatchType::Rgb)] + ), + :color_slider( + ColorSliderProps { + value: 0.5, + on_change: callback( + |change: In>, mut color: ResMut| { + color.rgb_color.red = change.value; + }, + ), + channel: ColorChannel::Red + }, + ), + :color_slider( + ColorSliderProps { + value: 0.5, + on_change: callback( + |change: In>, mut color: ResMut| { + color.rgb_color.green = change.value; + }, + ), + channel: ColorChannel::Green + }, + ), + :color_slider( + ColorSliderProps { + value: 0.5, + on_change: callback( + |change: In>, mut color: ResMut| { + color.rgb_color.blue = change.value; + }, + ), + channel: ColorChannel::Blue + }, + ), + :color_slider( + ColorSliderProps { + value: 0.5, + on_change: callback( + |change: In>, mut color: ResMut| { + color.rgb_color.alpha = change.value; + }, + ), + channel: ColorChannel::Alpha + }, + ), + ( + Node { + justify_content: JustifyContent::SpaceBetween, + } + [Text("Hsl"), (color_swatch() SwatchType::Hsl)] + ), + :color_slider( + ColorSliderProps { + value: 0.5, + on_change: callback( + |change: In>, mut color: ResMut| { + color.hsl_color.hue = change.value; + }, + ), + channel: ColorChannel::HslHue + }, + ), + :color_slider( + ColorSliderProps { + value: 0.5, + on_change: callback( + |change: In>, mut color: ResMut| { + color.hsl_color.saturation = change.value; + }, + ), + channel: ColorChannel::HslSaturation + }, + ), + :color_slider( + ColorSliderProps { + value: 0.5, + on_change: callback( + |change: In>, mut color: ResMut| { + color.hsl_color.lightness = change.value; + }, + ), + channel: ColorChannel::HslLightness + }, + ), + color_swatch(), + ] + ], + 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.), + } [ ( - Node { - display: Display::Flex, - flex_direction: FlexDirection::Row, - justify_content: JustifyContent::SpaceBetween, - ..default() - }, - children![Text("Srgba".to_owned()), color_swatch(SwatchType::Rgb),] - ), - color_slider( - ColorSliderProps { - value: 0.5, - on_change: Callback::System(change_red), - channel: ColorChannel::Red - }, - () - ), - color_slider( - ColorSliderProps { - value: 0.5, - on_change: Callback::System(change_green), - channel: ColorChannel::Green - }, - () - ), - color_slider( - ColorSliderProps { - value: 0.5, - on_change: Callback::System(change_blue), - channel: ColorChannel::Blue - }, - () - ), - color_slider( - ColorSliderProps { - value: 0.5, - on_change: Callback::System(change_alpha), - channel: ColorChannel::Alpha - }, - () + :subpane [ + :subpane_header [ + (Text("Left") ThemedText), + (Text("Center") ThemedText), + (Text("Right") ThemedText) + ], + :subpane_body [ + (Text("Body") ThemedText), + ], + ] ), ( - Node { - display: Display::Flex, - flex_direction: FlexDirection::Row, - justify_content: JustifyContent::SpaceBetween, - ..default() - }, - children![Text("Hsl".to_owned()), color_swatch(SwatchType::Hsl),] - ), - color_slider( - ColorSliderProps { - value: 0.5, - on_change: Callback::System(change_hue), - channel: ColorChannel::HslHue - }, - () - ), - color_slider( - ColorSliderProps { - value: 0.5, - on_change: Callback::System(change_saturation), - channel: ColorChannel::HslSaturation - }, - () - ), - color_slider( - ColorSliderProps { - value: 0.5, - on_change: Callback::System(change_lightness), - channel: ColorChannel::HslLightness - }, - () + :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) + ), + ] ) ] - ),], - ) + ] + } } fn update_colors( colors: Res, mut sliders: Query<(Entity, &ColorSlider, &mut SliderBaseColor)>, + new_sliders: Query<(), Added>, swatches: Query<(&SwatchType, &Children), With>, mut commands: Commands, ) { - if colors.is_changed() { + if colors.is_changed() || !new_sliders.is_empty() { for (slider_ent, slider, mut base) in sliders.iter_mut() { match slider.channel { ColorChannel::Red => {