From d5882d45d71ebfa805747bbb4bbf5fb226e85581 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 14 Jul 2025 15:00:32 -0700 Subject: [PATCH 01/13] Next Generation Bevy Scenes --- Cargo.toml | 17 +- crates/bevy_a11y/src/lib.rs | 2 +- crates/bevy_animation/src/graph.rs | 8 +- crates/bevy_asset/src/handle.rs | 48 +- crates/bevy_asset/src/lib.rs | 14 +- crates/bevy_asset/src/server/mod.rs | 14 + .../src/auto_exposure/settings.rs | 5 +- crates/bevy_core_widgets/src/callback.rs | 110 ++++- crates/bevy_core_widgets/src/core_button.rs | 3 +- crates/bevy_core_widgets/src/core_checkbox.rs | 3 +- crates/bevy_core_widgets/src/core_radio.rs | 5 +- crates/bevy_core_widgets/src/core_slider.rs | 3 +- crates/bevy_core_widgets/src/lib.rs | 2 +- crates/bevy_ecs/Cargo.toml | 1 + crates/bevy_ecs/macros/src/lib.rs | 14 + crates/bevy_ecs/macros/src/template.rs | 367 ++++++++++++++ .../bevy_ecs/macros/src/variant_defaults.rs | 55 +++ crates/bevy_ecs/src/entity/entity_path.rs | 51 ++ crates/bevy_ecs/src/entity/mod.rs | 6 +- crates/bevy_ecs/src/lib.rs | 4 + crates/bevy_ecs/src/name.rs | 59 +-- crates/bevy_ecs/src/template.rs | 190 ++++++++ crates/bevy_feathers/Cargo.toml | 1 + crates/bevy_feathers/src/controls/button.rs | 55 +-- crates/bevy_feathers/src/controls/checkbox.rs | 106 ++-- crates/bevy_feathers/src/controls/radio.rs | 91 ++-- crates/bevy_feathers/src/controls/slider.rs | 63 ++- .../src/controls/toggle_switch.rs | 56 +-- crates/bevy_feathers/src/font_styles.rs | 47 +- crates/bevy_feathers/src/theme.rs | 8 +- crates/bevy_gizmos/src/retained.rs | 16 +- crates/bevy_image/src/texture_atlas.rs | 11 +- crates/bevy_internal/Cargo.toml | 1 + crates/bevy_internal/src/default_plugins.rs | 2 + crates/bevy_internal/src/lib.rs | 2 + crates/bevy_mesh/src/components.rs | 16 +- crates/bevy_mesh/src/skinning.rs | 13 +- crates/bevy_pbr/src/lightmap/mod.rs | 2 +- crates/bevy_pbr/src/wireframe.rs | 8 +- crates/bevy_platform/src/hash.rs | 7 + .../src/impls/bevy_platform/hash.rs | 4 +- .../src/render_resource/pipeline.rs | 42 +- crates/bevy_scene/src/components.rs | 16 +- crates/bevy_scene2/Cargo.toml | 21 + crates/bevy_scene2/macros/Cargo.toml | 19 + crates/bevy_scene2/macros/src/bsn/codegen.rs | 371 ++++++++++++++ crates/bevy_scene2/macros/src/bsn/mod.rs | 26 + crates/bevy_scene2/macros/src/bsn/parse.rs | 453 ++++++++++++++++++ crates/bevy_scene2/macros/src/bsn/types.rs | 99 ++++ crates/bevy_scene2/macros/src/lib.rs | 13 + crates/bevy_scene2/src/lib.rs | 97 ++++ crates/bevy_scene2/src/resolved_scene.rs | 78 +++ crates/bevy_scene2/src/scene.rs | 169 +++++++ crates/bevy_scene2/src/scene_list.rs | 101 ++++ crates/bevy_scene2/src/scene_patch.rs | 33 ++ crates/bevy_scene2/src/spawn.rs | 107 +++++ crates/bevy_sprite/src/mesh2d/wireframe2d.rs | 8 +- crates/bevy_sprite/src/sprite.rs | 17 +- crates/bevy_sprite/src/tilemap_chunk/mod.rs | 13 +- crates/bevy_text/src/text.rs | 2 +- crates/bevy_ui/src/interaction_states.rs | 2 +- crates/bevy_winit/src/custom_cursor.rs | 15 +- examples/2d/texture_atlas.rs | 8 +- examples/asset/asset_decompression.rs | 11 +- examples/asset/custom_asset.rs | 13 +- examples/games/alien_cake_addict.rs | 12 +- examples/scene/bsn.rs | 197 ++++++++ examples/scene/ui_scene.rs | 67 +++ examples/ui/feathers.rs | 320 +++++-------- 69 files changed, 3298 insertions(+), 522 deletions(-) create mode 100644 crates/bevy_ecs/macros/src/template.rs create mode 100644 crates/bevy_ecs/macros/src/variant_defaults.rs create mode 100644 crates/bevy_ecs/src/entity/entity_path.rs create mode 100644 crates/bevy_ecs/src/template.rs create mode 100644 crates/bevy_scene2/Cargo.toml create mode 100644 crates/bevy_scene2/macros/Cargo.toml create mode 100644 crates/bevy_scene2/macros/src/bsn/codegen.rs create mode 100644 crates/bevy_scene2/macros/src/bsn/mod.rs create mode 100644 crates/bevy_scene2/macros/src/bsn/parse.rs create mode 100644 crates/bevy_scene2/macros/src/bsn/types.rs create mode 100644 crates/bevy_scene2/macros/src/lib.rs create mode 100644 crates/bevy_scene2/src/lib.rs create mode 100644 crates/bevy_scene2/src/resolved_scene.rs create mode 100644 crates/bevy_scene2/src/scene.rs create mode 100644 crates/bevy_scene2/src/scene_list.rs create mode 100644 crates/bevy_scene2/src/scene_patch.rs create mode 100644 crates/bevy_scene2/src/spawn.rs create mode 100644 examples/scene/bsn.rs create mode 100644 examples/scene/ui_scene.rs diff --git a/Cargo.toml b/Cargo.toml index f047040bdc9f7..971b4b7f7567e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,6 +145,7 @@ default = [ "bevy_picking", "bevy_render", "bevy_scene", + "bevy_scene2", "bevy_sprite", "bevy_sprite_picking_backend", "bevy_state", @@ -167,6 +168,7 @@ default = [ "x11", "debug", "zstd_rust", + "experimental_bevy_feathers", ] # Recommended defaults for no_std applications @@ -249,6 +251,9 @@ bevy_render = ["bevy_internal/bevy_render", "bevy_color"] # Provides scene functionality bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"] +# Provides scene functionality +bevy_scene2 = ["bevy_internal/bevy_scene2", "bevy_asset"] + # Provides raytraced lighting (experimental) bevy_solari = [ "bevy_internal/bevy_solari", @@ -598,8 +603,9 @@ flate2 = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1.0.140" bytemuck = "1" -bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false } # The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself. +bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false } +bevy_scene2 = { path = "crates/bevy_scene2", version = "0.17.0-dev", default-features = false } bevy_ecs = { path = "crates/bevy_ecs", version = "0.17.0-dev", default-features = false } bevy_state = { path = "crates/bevy_state", version = "0.17.0-dev", default-features = false } bevy_asset = { path = "crates/bevy_asset", version = "0.17.0-dev", default-features = false } @@ -619,6 +625,7 @@ anyhow = "1" macro_rules_attribute = "0.2" accesskit = "0.19" nonmax = "0.5" +variadics_please = "1" [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] smol = "2" @@ -2788,6 +2795,14 @@ description = "Demonstrates loading from and saving scenes to files" category = "Scene" wasm = false +[[example]] +name = "bsn" +path = "examples/scene/bsn.rs" + +[[example]] +name = "ui_scene" +path = "examples/scene/ui_scene.rs" + # Shaders [[package.metadata.example_category]] name = "Shaders" diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs index 22b2f71f075e9..bcf7ae70e972d 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -120,7 +120,7 @@ impl ManageAccessibilityUpdates { /// /// If the entity doesn't have a parent, or if the immediate parent doesn't have /// an `AccessibilityNode`, its node will be an immediate child of the primary window. -#[derive(Component, Clone, Deref, DerefMut)] +#[derive(Component, Clone, Deref, DerefMut, Default)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct AccessibilityNode(pub Node); diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index adb4a7c7ac541..8c8402115b37b 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -131,10 +131,16 @@ pub struct AnimationGraph { } /// A [`Handle`] to the [`AnimationGraph`] to be used by the [`AnimationPlayer`](crate::AnimationPlayer) on the same entity. -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Clone)] pub struct AnimationGraphHandle(pub Handle); +impl Default for AnimationGraphHandle { + fn default() -> Self { + Self(Handle::default()) + } +} + impl From for AssetId { fn from(handle: AnimationGraphHandle) -> Self { handle.id() diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index 838c618d8ed1b..12ed0be85341e 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -1,9 +1,14 @@ use crate::{ - meta::MetaTransform, Asset, AssetId, AssetIndexAllocator, AssetPath, InternalAssetId, - UntypedAssetId, + meta::MetaTransform, Asset, AssetId, AssetIndexAllocator, AssetPath, AssetServer, + InternalAssetId, UntypedAssetId, }; use alloc::sync::Arc; -use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath}; +use bevy_ecs::{ + error::Result, + template::{GetTemplate, Template}, + world::EntityWorldMut, +}; +use bevy_reflect::{Reflect, TypePath}; use core::{ any::TypeId, hash::{Hash, Hasher}, @@ -130,7 +135,7 @@ impl core::fmt::Debug for StrongHandle { /// /// [`Handle::Strong`], via [`StrongHandle`] also provides access to useful [`Asset`] metadata, such as the [`AssetPath`] (if it exists). #[derive(Reflect)] -#[reflect(Default, Debug, Hash, PartialEq, Clone)] +#[reflect(Debug, Hash, PartialEq, Clone)] pub enum Handle { /// A "strong" reference to a live (or loading) [`Asset`]. If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept /// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata. @@ -150,6 +155,9 @@ impl Clone for Handle { } impl Handle { + pub fn default() -> Self { + Handle::Uuid(AssetId::::DEFAULT_UUID, PhantomData) + } /// Returns the [`AssetId`] of this [`Asset`]. #[inline] pub fn id(&self) -> AssetId { @@ -189,9 +197,37 @@ impl Handle { } } -impl Default for Handle { +impl GetTemplate for Handle { + type Template = HandleTemplate; +} + +pub struct HandleTemplate { + path: AssetPath<'static>, + marker: PhantomData, +} + +impl Default for HandleTemplate { fn default() -> Self { - Handle::Uuid(AssetId::::DEFAULT_UUID, PhantomData) + Self { + path: Default::default(), + marker: Default::default(), + } + } +} + +impl>, T> From for HandleTemplate { + fn from(value: I) -> Self { + Self { + path: value.into(), + marker: PhantomData, + } + } +} + +impl Template for HandleTemplate { + type Output = Handle; + fn build(&mut self, entity: &mut EntityWorldMut) -> Result> { + Ok(entity.resource::().load(&self.path)) } } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 8186b6315d5b4..d3c51ec9eb8f8 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -472,6 +472,12 @@ impl VisitAssetDependencies for Option> { } } +impl VisitAssetDependencies for UntypedAssetId { + fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + visit(*self); + } +} + impl VisitAssetDependencies for UntypedHandle { fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { visit(self.id()); @@ -486,18 +492,18 @@ impl VisitAssetDependencies for Option { } } -impl VisitAssetDependencies for Vec> { +impl VisitAssetDependencies for Vec { fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { for dependency in self { - visit(dependency.id().untyped()); + dependency.visit_dependencies(visit); } } } -impl VisitAssetDependencies for Vec { +impl VisitAssetDependencies for HashSet, S> { fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { for dependency in self { - visit(dependency.id()); + visit(dependency.id().untyped()); } } } diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 69dc8428da87d..da3d59899955f 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -864,6 +864,20 @@ impl AssetServer { self.load_asset(LoadedAsset::new_with_dependencies(asset)) } + // TODO: this is a hack: this allows the asset to pretend to be from the path, but this will cause issues in practice + #[must_use = "not using the returned strong handle may result in the unexpected release of the asset"] + pub fn load_with_path<'a, A: Asset>( + &self, + path: impl Into>, + asset: A, + ) -> Handle { + let loaded_asset: LoadedAsset = asset.into(); + let erased_loaded_asset: ErasedLoadedAsset = loaded_asset.into(); + let path: AssetPath = path.into(); + self.load_asset_untyped(Some(path.into_owned()), erased_loaded_asset) + .typed_debug_checked() + } + pub(crate) fn load_asset(&self, asset: impl Into>) -> Handle { let loaded_asset: LoadedAsset = asset.into(); let erased_loaded_asset: ErasedLoadedAsset = loaded_asset.into(); diff --git a/crates/bevy_core_pipeline/src/auto_exposure/settings.rs b/crates/bevy_core_pipeline/src/auto_exposure/settings.rs index ae359a8a01dd4..ecee10553b239 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/settings.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/settings.rs @@ -6,7 +6,6 @@ use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; use bevy_image::Image; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{extract_component::ExtractComponent, view::Hdr}; -use bevy_utils::default; /// Component that enables auto exposure for an HDR-enabled 2d or 3d camera. /// @@ -97,8 +96,8 @@ impl Default for AutoExposure { speed_brighten: 3.0, speed_darken: 1.0, exponential_transition_distance: 1.5, - metering_mask: default(), - compensation_curve: default(), + metering_mask: Handle::default(), + compensation_curve: Handle::default(), } } } diff --git a/crates/bevy_core_widgets/src/callback.rs b/crates/bevy_core_widgets/src/callback.rs index 37905e221cfcf..c3081837a785d 100644 --- a/crates/bevy_core_widgets/src/callback.rs +++ b/crates/bevy_core_widgets/src/callback.rs @@ -1,5 +1,7 @@ -use bevy_ecs::system::{Commands, SystemId, SystemInput}; +use bevy_ecs::system::{Commands, IntoSystem, SystemId, SystemInput}; +use bevy_ecs::template::{GetTemplate, Template}; use bevy_ecs::world::{DeferredWorld, World}; +use std::marker::PhantomData; /// A callback defines how we want to be notified when a widget changes state. Unlike an event /// or observer, callbacks are intended for "point-to-point" communication that cuts across the @@ -27,15 +29,119 @@ use bevy_ecs::world::{DeferredWorld, World}; /// // Later, when we want to execute the callback: /// app.world_mut().commands().notify(&callback); /// ``` -#[derive(Default, Debug)] +#[derive(Debug)] pub enum Callback { /// Invoke a one-shot system System(SystemId), /// Ignore this notification + Ignore, +} + +impl Copy for Callback {} +impl Clone for Callback { + fn clone(&self) -> Self { + match self { + Self::System(arg0) => Self::System(arg0.clone()), + Self::Ignore => Self::Ignore, + } + } +} + +impl GetTemplate for Callback { + type Template = CallbackTemplate; +} + +#[derive(Default)] +pub enum CallbackTemplate { + System(Box>), + SystemId(SystemId), #[default] Ignore, } +impl CallbackTemplate { + pub fn clone(&self) -> CallbackTemplate { + match self { + CallbackTemplate::System(register_system) => { + CallbackTemplate::System(register_system.box_clone()) + } + CallbackTemplate::SystemId(system_id) => CallbackTemplate::SystemId(*system_id), + CallbackTemplate::Ignore => CallbackTemplate::Ignore, + } + } +} + +pub trait RegisterSystem: Send + Sync + 'static { + fn register_system(&mut self, world: &mut World) -> SystemId; + fn box_clone(&self) -> Box>; +} + +pub struct IntoWrapper { + into_system: Option, + marker: PhantomData (In, Marker)>, +} + +pub fn callback< + I: IntoSystem + Send + Sync + Clone + 'static, + In: SystemInput + 'static, + Marker: 'static, +>( + system: I, +) -> CallbackTemplate { + CallbackTemplate::from(IntoWrapper { + into_system: Some(system), + marker: PhantomData, + }) +} + +impl< + I: IntoSystem + Clone + Send + Sync + 'static, + In: SystemInput + 'static, + Marker: 'static, + > RegisterSystem for IntoWrapper +{ + fn register_system(&mut self, world: &mut World) -> SystemId { + world.register_system(self.into_system.take().unwrap()) + } + + fn box_clone(&self) -> Box> { + Box::new(IntoWrapper { + into_system: self.into_system.clone(), + marker: PhantomData, + }) + } +} + +impl< + I: IntoSystem + Clone + Send + Sync + 'static, + In: SystemInput + 'static, + Marker: 'static, + > From> for CallbackTemplate +{ + fn from(value: IntoWrapper) -> Self { + CallbackTemplate::System(Box::new(value)) + } +} + +impl Template for CallbackTemplate { + type Output = Callback; + + fn build( + &mut self, + entity: &mut bevy_ecs::world::EntityWorldMut, + ) -> bevy_ecs::error::Result { + Ok(match self { + CallbackTemplate::System(register) => { + let id = entity.world_scope(move |world| register.register_system(world)); + *self = CallbackTemplate::SystemId(id); + Callback::System(id) + } + CallbackTemplate::SystemId(id) => Callback::System(*id), + CallbackTemplate::Ignore => Callback::Ignore, + }) + } +} + /// Trait used to invoke a [`Callback`], unifying the API across callers. pub trait Notify { /// Invoke the callback with no arguments. diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs index 5ef0d33ef0c30..4e1087e789a88 100644 --- a/crates/bevy_core_widgets/src/core_button.rs +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -17,11 +17,12 @@ use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; use bevy_ui::{InteractionDisabled, Pressed}; use crate::{Activate, Callback, Notify}; +use bevy_ecs::template::GetTemplate; /// Headless button widget. This widget maintains a "pressed" state, which is used to /// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked` /// event when the button is un-pressed. -#[derive(Component, Default, Debug)] +#[derive(Component, Clone, Debug, GetTemplate)] #[require(AccessibilityNode(accesskit::Node::new(Role::Button)))] pub struct CoreButton { /// Callback to invoke when the button is clicked, or when the `Enter` or `Space` key diff --git a/crates/bevy_core_widgets/src/core_checkbox.rs b/crates/bevy_core_widgets/src/core_checkbox.rs index 01e3e61e49acd..db7f013abb1fe 100644 --- a/crates/bevy_core_widgets/src/core_checkbox.rs +++ b/crates/bevy_core_widgets/src/core_checkbox.rs @@ -16,6 +16,7 @@ use bevy_picking::events::{Click, Pointer}; use bevy_ui::{Checkable, Checked, InteractionDisabled}; use crate::{Callback, Notify as _, ValueChange}; +use bevy_ecs::template::GetTemplate; /// Headless widget implementation for checkboxes. The [`Checked`] component represents the current /// state of the checkbox. The `on_change` field is an optional system id that will be run when the @@ -28,7 +29,7 @@ use crate::{Callback, Notify as _, ValueChange}; /// The [`CoreCheckbox`] component can be used to implement other kinds of toggle widgets. If you /// are going to do a toggle switch, you should override the [`AccessibilityNode`] component with /// the `Switch` role instead of the `Checkbox` role. -#[derive(Component, Debug, Default)] +#[derive(Component, Debug, Clone, GetTemplate)] #[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)] pub struct CoreCheckbox { /// One-shot system that is run when the checkbox state needs to be changed. If this value is diff --git a/crates/bevy_core_widgets/src/core_radio.rs b/crates/bevy_core_widgets/src/core_radio.rs index 0aeebe9825cf0..ff4f10dbe3b22 100644 --- a/crates/bevy_core_widgets/src/core_radio.rs +++ b/crates/bevy_core_widgets/src/core_radio.rs @@ -17,6 +17,7 @@ use bevy_picking::events::{Click, Pointer}; use bevy_ui::{Checkable, Checked, InteractionDisabled}; use crate::{Activate, Callback, Notify}; +use bevy_ecs::template::GetTemplate; /// Headless widget implementation for a "radio button group". This component is used to group /// multiple [`CoreRadio`] components together, allowing them to behave as a single unit. It @@ -33,7 +34,7 @@ use crate::{Activate, Callback, Notify}; /// associated with a particular constant value, and would be checked whenever that value is equal /// to the group's value. This also means that as long as each button's associated value is unique /// within the group, it should never be the case that more than one button is selected at a time. -#[derive(Component, Debug)] +#[derive(Component, Debug, Clone, GetTemplate)] #[require(AccessibilityNode(accesskit::Node::new(Role::RadioGroup)))] pub struct CoreRadioGroup { /// Callback which is called when the selected radio button changes. @@ -46,7 +47,7 @@ pub struct CoreRadioGroup { /// According to the WAI-ARIA best practices document, radio buttons should not be focusable, /// but rather the enclosing group should be focusable. /// See / -#[derive(Component, Debug)] +#[derive(Component, Debug, Clone, Default)] #[require(AccessibilityNode(accesskit::Node::new(Role::RadioButton)), Checkable)] pub struct CoreRadio; diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index 9f38065e374bc..a1eb3b62a1177 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -24,6 +24,7 @@ use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; use crate::{Callback, Notify, ValueChange}; +use bevy_ecs::template::GetTemplate; /// Defines how the slider should behave when you click on the track (not the thumb). #[derive(Debug, Default, PartialEq, Clone, Copy)] @@ -66,7 +67,7 @@ pub enum TrackClick { /// /// In cases where overhang is desired for artistic reasons, the thumb may have additional /// decorative child elements, absolutely positioned, which don't affect the size measurement. -#[derive(Component, Debug, Default)] +#[derive(Component, Debug, GetTemplate, Clone)] #[require( AccessibilityNode(accesskit::Node::new(Role::Slider)), CoreSliderDragState, diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs index 9a20b59c13032..ae751268a8b58 100644 --- a/crates/bevy_core_widgets/src/lib.rs +++ b/crates/bevy_core_widgets/src/lib.rs @@ -24,7 +24,7 @@ mod core_slider; use bevy_app::{PluginGroup, PluginGroupBuilder}; use bevy_ecs::entity::Entity; -pub use callback::{Callback, Notify}; +pub use callback::{callback, Callback, CallbackTemplate, Notify}; pub use core_button::{CoreButton, CoreButtonPlugin}; pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked}; pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin}; diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index f0f9b782afff2..b0c050cd8afd8 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -121,6 +121,7 @@ variadics_please = { version = "1.1", default-features = false } tracing = { version = "0.1", default-features = false, optional = true } log = { version = "0.4", default-features = false } bumpalo = "3" +downcast-rs = { version = "2", default-features = false, features = ["std"] } subsecond = { version = "0.7.0-alpha.1", optional = true } slotmap = { version = "1.0.7", default-features = false } diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 7b388f4a1446a..c2b541efc6c78 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -7,6 +7,8 @@ extern crate proc_macro; mod component; mod query_data; mod query_filter; +mod template; +mod variant_defaults; mod world_query; use crate::{ @@ -730,3 +732,15 @@ pub fn derive_from_world(input: TokenStream) -> TokenStream { } }) } + +/// Derives GetTemplate. +#[proc_macro_derive(GetTemplate, attributes(template, default))] +pub fn derive_get_template(input: TokenStream) -> TokenStream { + template::derive_get_template(input) +} + +/// Derives VariantDefaults. +#[proc_macro_derive(VariantDefaults)] +pub fn derive_variant_defaults(input: TokenStream) -> TokenStream { + variant_defaults::derive_variant_defaults(input) +} diff --git a/crates/bevy_ecs/macros/src/template.rs b/crates/bevy_ecs/macros/src/template.rs new file mode 100644 index 0000000000000..2e451d58060a2 --- /dev/null +++ b/crates/bevy_ecs/macros/src/template.rs @@ -0,0 +1,367 @@ +use bevy_macro_utils::BevyManifest; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, Data, DeriveInput, Fields, FieldsUnnamed, Index, Path}; + +const TEMPLATE_ATTRIBUTE: &str = "template"; +const TEMPLATE_DEFAULT_ATTRIBUTE: &str = "default"; + +pub(crate) fn derive_get_template(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let manifest = BevyManifest::shared(); + let bevy_ecs = manifest.get_path("bevy_ecs"); + + let type_ident = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + let template_ident = format_ident!("{type_ident}Template"); + + let is_pub = matches!(ast.vis, syn::Visibility::Public(_)); + let maybe_pub = if is_pub { quote!(pub) } else { quote!() }; + + let template = match &ast.data { + Data::Struct(data_struct) => { + let StructImpl { + template_fields, + template_field_builds, + template_field_defaults, + .. + } = struct_impl(&data_struct.fields, &bevy_ecs, false); + match &data_struct.fields { + Fields::Named(_) => { + quote! { + #[allow(missing_docs)] + #maybe_pub struct #template_ident #impl_generics #where_clause { + #(#template_fields,)* + } + + impl #impl_generics #bevy_ecs::template::Template for #template_ident #type_generics #where_clause { + type Output = #type_ident #type_generics; + fn build(&mut self, entity: &mut #bevy_ecs::world::EntityWorldMut) -> #bevy_ecs::error::Result { + Ok(#type_ident { + #(#template_field_builds,)* + }) + } + } + + impl #impl_generics Default for #template_ident #type_generics #where_clause { + fn default() -> Self { + Self { + #(#template_field_defaults,)* + } + } + } + } + } + Fields::Unnamed(_) => { + quote! { + #[allow(missing_docs)] + #maybe_pub struct #template_ident #impl_generics ( + #(#template_fields,)* + ) #where_clause; + + impl #impl_generics #bevy_ecs::template::Template for #template_ident #type_generics #where_clause { + type Output = #type_ident #type_generics; + fn build(&mut self, entity: &mut #bevy_ecs::world::EntityWorldMut) -> #bevy_ecs::error::Result { + Ok(#type_ident ( + #(#template_field_builds,)* + )) + } + } + + impl #impl_generics Default for #template_ident #type_generics #where_clause { + fn default() -> Self { + Self ( + #(#template_field_defaults,)* + ) + } + } + } + } + Fields::Unit => { + quote! { + #[allow(missing_docs)] + #maybe_pub struct #template_ident; + + impl #impl_generics #bevy_ecs::template::Template for #template_ident #type_generics #where_clause { + type Output = #type_ident; + fn build(&mut self, entity: &mut #bevy_ecs::world::EntityWorldMut) -> #bevy_ecs::error::Result { + Ok(#type_ident) + } + } + + impl #impl_generics Default for #template_ident #type_generics #where_clause { + fn default() -> Self { + Self + } + } + } + } + } + } + Data::Enum(data_enum) => { + let mut variant_definitions = Vec::new(); + let mut variant_builds = Vec::new(); + let mut variant_default_ident = None; + let mut variant_defaults = Vec::new(); + for variant in &data_enum.variants { + let StructImpl { + template_fields, + template_field_builds, + template_field_defaults, + .. + } = struct_impl(&variant.fields, &bevy_ecs, true); + + let is_default = variant + .attrs + .iter() + .find(|a| a.path().is_ident(TEMPLATE_DEFAULT_ATTRIBUTE)) + .is_some(); + if is_default { + if variant_default_ident.is_some() { + panic!("Cannot have multiple default variants"); + } + } + let variant_ident = &variant.ident; + let variant_name_lower = variant_ident.to_string().to_lowercase(); + let variant_default_name = format_ident!("default_{}", variant_name_lower); + match &variant.fields { + Fields::Named(fields) => { + variant_definitions.push(quote! { + #variant_ident { + #(#template_fields,)* + } + }); + let field_idents = fields.named.iter().map(|f| &f.ident); + variant_builds.push(quote! { + // TODO: proper assignments here + #template_ident::#variant_ident { + #(#field_idents,)* + } => { + #type_ident::#variant_ident { + #(#template_field_builds,)* + } + } + }); + + if is_default { + variant_default_ident = Some(quote! { + Self::#variant_ident { + #(#template_field_defaults,)* + } + }); + } + variant_defaults.push(quote! { + #maybe_pub fn #variant_default_name() -> Self { + Self::#variant_ident { + #(#template_field_defaults,)* + } + } + }) + } + Fields::Unnamed(FieldsUnnamed { unnamed: f, .. }) => { + let field_idents = + f.iter().enumerate().map(|(i, _)| format_ident!("t{}", i)); + variant_definitions.push(quote! { + #variant_ident(#(#template_fields,)*) + }); + variant_builds.push(quote! { + // TODO: proper assignments here + #template_ident::#variant_ident( + #(#field_idents,)* + ) => { + #type_ident::#variant_ident( + #(#template_field_builds,)* + ) + } + }); + if is_default { + variant_default_ident = Some(quote! { + Self::#variant_ident( + #(#template_field_defaults,)* + ) + }); + } + + variant_defaults.push(quote! { + #maybe_pub fn #variant_default_name() -> Self { + Self::#variant_ident( + #(#template_field_defaults,)* + ) + } + }) + } + Fields::Unit => { + variant_definitions.push(quote! {#variant_ident}); + variant_builds.push( + quote! {#template_ident::#variant_ident => #type_ident::#variant_ident}, + ); + if is_default { + variant_default_ident = Some(quote! { + Self::#variant_ident + }); + } + variant_defaults.push(quote! { + #maybe_pub fn #variant_default_name() -> Self { + Self::#variant_ident + } + }) + } + } + } + + if variant_default_ident.is_none() { + panic!("Deriving Template for enums requires picking a default variant using #[default]"); + } + + quote! { + #[allow(missing_docs)] + #maybe_pub enum #template_ident #type_generics #where_clause { + #(#variant_definitions,)* + } + + impl #impl_generics #template_ident #type_generics #where_clause { + #(#variant_defaults)* + } + + impl #impl_generics #bevy_ecs::template::Template for #template_ident #type_generics #where_clause { + type Output = #type_ident #type_generics; + fn build(&mut self, entity: &mut #bevy_ecs::world::EntityWorldMut) -> #bevy_ecs::error::Result { + Ok(match self { + #(#variant_builds,)* + }) + } + } + + impl #impl_generics Default for #template_ident #type_generics #where_clause { + fn default() -> Self { + #variant_default_ident + } + } + } + } + Data::Union(_) => panic!("Union types are not supported yet."), + }; + + TokenStream::from(quote! { + impl #impl_generics #bevy_ecs::template::GetTemplate for #type_ident #type_generics #where_clause { + type Template = #template_ident #type_generics; + } + + #template + }) +} + +struct StructImpl { + template_fields: Vec, + template_field_builds: Vec, + template_field_defaults: Vec, +} + +fn struct_impl(fields: &Fields, bevy_ecs: &Path, is_enum: bool) -> StructImpl { + let mut template_fields = Vec::with_capacity(fields.len()); + let mut template_field_builds = Vec::with_capacity(fields.len()); + let mut template_field_defaults = Vec::with_capacity(fields.len()); + let is_named = matches!(fields, Fields::Named(_)); + for (index, field) in fields.iter().enumerate() { + let is_template = field + .attrs + .iter() + .find(|a| a.path().is_ident(TEMPLATE_ATTRIBUTE)) + .is_some(); + let is_pub = matches!(field.vis, syn::Visibility::Public(_)); + let field_maybe_pub = if is_pub { quote!(pub) } else { quote!() }; + let ident = &field.ident; + let ty = &field.ty; + let index = Index::from(index); + if is_named { + if is_template { + template_fields.push(quote! { + #field_maybe_pub #ident: #bevy_ecs::template::TemplateField<<#ty as #bevy_ecs::template::GetTemplate>::Template> + }); + if is_enum { + template_field_builds.push(quote! { + #ident: match #ident { + #bevy_ecs::template::TemplateField::Template(template) => template.build(entity)?, + #bevy_ecs::template::TemplateField::Value(value) => Clone::clone(value), + } + }); + } else { + template_field_builds.push(quote! { + #ident: match &mut self.#ident { + #bevy_ecs::template::TemplateField::Template(template) => template.build(entity)?, + #bevy_ecs::template::TemplateField::Value(value) => Clone::clone(value), + } + }); + } + template_field_defaults.push(quote! { + #ident: Default::default() + }); + } else { + template_fields.push(quote! { + #field_maybe_pub #ident: <#ty as #bevy_ecs::template::GetTemplate>::Template + }); + if is_enum { + template_field_builds.push(quote! { + #ident: #ident.build(entity)? + }); + } else { + template_field_builds.push(quote! { + #ident: self.#ident.build(entity)? + }); + } + + template_field_defaults.push(quote! { + #ident: Default::default() + }); + } + } else { + if is_template { + template_fields.push(quote! { + #field_maybe_pub #bevy_ecs::template::TemplateField<<#ty as #bevy_ecs::template::GetTemplate>::Template> + }); + if is_enum { + let enum_tuple_ident = format_ident!("t{}", index); + template_field_builds.push(quote! { + match #enum_tuple_ident { + #bevy_ecs::template::TemplateField::Template(template) => template.build(entity)?, + #bevy_ecs::template::TemplateField::Value(value) => Clone::clone(value), + } + }); + } else { + template_field_builds.push(quote! { + match &mut self.#index { + #bevy_ecs::template::TemplateField::Template(template) => template.build(entity)?, + #bevy_ecs::template::TemplateField::Value(value) => Clone::clone(value), + } + }); + } + template_field_defaults.push(quote! { + Default::default() + }); + } else { + template_fields.push(quote! { + #field_maybe_pub <#ty as #bevy_ecs::template::GetTemplate>::Template + }); + if is_enum { + let enum_tuple_ident = format_ident!("t{}", index); + template_field_builds.push(quote! { + #enum_tuple_ident.build(entity)? + }); + } else { + template_field_builds.push(quote! { + self.#index.build(entity)? + }); + } + template_field_defaults.push(quote! { + Default::default() + }); + } + } + } + StructImpl { + template_fields, + template_field_builds, + template_field_defaults, + } +} diff --git a/crates/bevy_ecs/macros/src/variant_defaults.rs b/crates/bevy_ecs/macros/src/variant_defaults.rs new file mode 100644 index 0000000000000..a7a565fc66d31 --- /dev/null +++ b/crates/bevy_ecs/macros/src/variant_defaults.rs @@ -0,0 +1,55 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, Data, DeriveInput}; + +pub(crate) fn derive_variant_defaults(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let type_ident = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + let Data::Enum(data_enum) = &ast.data else { + panic!("Can only derive VariantDefaults for enums"); + }; + + let mut variant_defaults = Vec::new(); + for variant in &data_enum.variants { + let variant_ident = &variant.ident; + let variant_name_lower = variant_ident.to_string().to_lowercase(); + let variant_default_name = format_ident!("default_{}", variant_name_lower); + match &variant.fields { + syn::Fields::Named(fields_named) => { + let fields = fields_named.named.iter().map(|f| &f.ident); + variant_defaults.push(quote! { + pub fn #variant_default_name() -> Self { + Self::#variant_ident { + #(#fields: Default::default(),)* + } + } + }) + } + syn::Fields::Unnamed(fields_unnamed) => { + let fields = fields_unnamed + .unnamed + .iter() + .map(|_| quote! {Default::default()}); + variant_defaults.push(quote! { + pub fn #variant_default_name() -> Self { + Self::#variant_ident( + #(#fields,)* + ) + } + }) + } + syn::Fields::Unit => variant_defaults.push(quote! { + pub fn #variant_default_name() -> Self { + Self::#variant_ident + } + }), + } + } + + TokenStream::from(quote! { + impl #impl_generics #type_ident #type_generics #where_clause { + #(#variant_defaults)* + } + }) +} diff --git a/crates/bevy_ecs/src/entity/entity_path.rs b/crates/bevy_ecs/src/entity/entity_path.rs new file mode 100644 index 0000000000000..744de7ca17143 --- /dev/null +++ b/crates/bevy_ecs/src/entity/entity_path.rs @@ -0,0 +1,51 @@ +use crate::{entity::Entity, world::EntityWorldMut}; +use log::warn; +use std::{borrow::Cow, string::String}; +use thiserror::Error; + +/// A path to an entity. +pub struct EntityPath<'a>(Cow<'a, str>); + +impl<'a> Default for EntityPath<'a> { + fn default() -> Self { + Self(Default::default()) + } +} + +impl<'a> From<&'a str> for EntityPath<'a> { + #[inline] + fn from(entity_path: &'a str) -> Self { + EntityPath(Cow::Borrowed(entity_path)) + } +} + +impl<'a> From<&'a String> for EntityPath<'a> { + #[inline] + fn from(entity_path: &'a String) -> Self { + EntityPath(Cow::Borrowed(entity_path.as_str())) + } +} + +impl From for EntityPath<'static> { + #[inline] + fn from(asset_path: String) -> Self { + EntityPath(Cow::Owned(asset_path.into())) + } +} + +/// An [`Error`] that occurs when failing to resolve an [`EntityPath`]. +#[derive(Error, Debug)] +pub enum ResolveEntityPathError {} + +impl<'w> EntityWorldMut<'w> { + /// Attempt to resolve the given `path` to an [`Entity`]. + pub fn resolve_path<'a>( + &self, + path: &EntityPath<'a>, + ) -> Result { + if !path.0.is_empty() { + warn!("Resolving non-empty entity paths doesn't work yet!"); + } + Ok(self.id()) + } +} diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 64a8c8952e0df..e17b78d0f1351 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -37,6 +37,7 @@ //! [`EntityWorldMut::remove`]: crate::world::EntityWorldMut::remove mod clone_entities; +mod entity_path; mod entity_set; mod map_entities; #[cfg(feature = "bevy_reflect")] @@ -45,7 +46,7 @@ use bevy_reflect::Reflect; use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; pub use clone_entities::*; -use derive_more::derive::Display; +pub use entity_path::*; pub use entity_set::*; pub use map_entities::*; @@ -68,7 +69,6 @@ pub mod unique_array; pub mod unique_slice; pub mod unique_vec; -use nonmax::NonMaxU32; pub use unique_array::{UniqueEntityArray, UniqueEntityEquivalentArray}; pub use unique_slice::{UniqueEntityEquivalentSlice, UniqueEntitySlice}; pub use unique_vec::{UniqueEntityEquivalentVec, UniqueEntityVec}; @@ -82,7 +82,9 @@ use crate::{ use alloc::vec::Vec; use bevy_platform::sync::atomic::Ordering; use core::{fmt, hash::Hash, mem, num::NonZero, panic::Location}; +use derive_more::derive::Display; use log::warn; +use nonmax::NonMaxU32; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 96fd542b61bc3..e3fffe85d1ade 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -54,6 +54,7 @@ pub mod schedule; pub mod spawn; pub mod storage; pub mod system; +pub mod template; pub mod traversal; pub mod world; @@ -104,6 +105,7 @@ pub mod prelude { Res, ResMut, Single, System, SystemIn, SystemInput, SystemParamBuilder, SystemParamFunction, When, }, + template::{GetTemplate, Template}, world::{ EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, FromWorld, World, @@ -125,6 +127,8 @@ pub mod prelude { pub use crate::reflect::AppFunctionRegistry; } +pub use bevy_ecs_macros::VariantDefaults; + /// Exports used by macros. /// /// These are not meant to be used directly and are subject to breaking changes. diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index 317c8f5017bb5..6ae64e173f373 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -6,9 +6,9 @@ use alloc::{ borrow::{Cow, ToOwned}, string::String, }; -use bevy_platform::hash::FixedHasher; +use bevy_platform::hash::Hashed; use core::{ - hash::{BuildHasher, Hash, Hasher}, + hash::{Hash, Hasher}, ops::Deref, }; @@ -47,10 +47,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; all(feature = "serialize", feature = "bevy_reflect"), reflect(Deserialize, Serialize) )] -pub struct Name { - hash: u64, // Won't be serialized - name: Cow<'static, str>, -} +pub struct Name(pub HashedStr); impl Default for Name { fn default() -> Self { @@ -58,15 +55,28 @@ impl Default for Name { } } +/// A wrapper over Hashed. This exists to make Name("value".into()) possible, which plays nicely with contexts like the `bsn!` macro. +#[derive(Reflect, Clone)] +pub struct HashedStr(Hashed>); + +impl From<&'static str> for HashedStr { + fn from(value: &'static str) -> Self { + Self(Hashed::new(Cow::Borrowed(value))) + } +} + +impl From for HashedStr { + fn from(value: String) -> Self { + Self(Hashed::new(Cow::Owned(value))) + } +} + impl Name { /// Creates a new [`Name`] from any string-like type. /// /// The internal hash will be computed immediately. pub fn new(name: impl Into>) -> Self { - let name = name.into(); - let mut name = Name { name, hash: 0 }; - name.update_hash(); - name + Self(HashedStr(Hashed::new(name.into()))) } /// Sets the entity's name. @@ -82,33 +92,28 @@ impl Name { /// This will allocate a new string if the name was previously /// created from a borrow. #[inline(always)] - pub fn mutate(&mut self, f: F) { - f(self.name.to_mut()); - self.update_hash(); + pub fn mutate(&mut self, _f: F) { + todo!("Expose this functionality in Hashed") } /// Gets the name of the entity as a `&str`. #[inline(always)] pub fn as_str(&self) -> &str { - &self.name - } - - fn update_hash(&mut self) { - self.hash = FixedHasher.hash_one(&self.name); + &self.0 .0 } } impl core::fmt::Display for Name { #[inline(always)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::Display::fmt(&self.name, f) + core::fmt::Display::fmt(&*self.0 .0, f) } } impl core::fmt::Debug for Name { #[inline(always)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::Debug::fmt(&self.name, f) + core::fmt::Debug::fmt(&self.0 .0, f) } } @@ -172,7 +177,7 @@ impl From for Name { impl AsRef for Name { #[inline(always)] fn as_ref(&self) -> &str { - &self.name + &self.0 .0 } } @@ -186,24 +191,24 @@ impl From<&Name> for String { impl From for String { #[inline(always)] fn from(val: Name) -> String { - val.name.into_owned() + val.as_str().to_owned() } } impl Hash for Name { fn hash(&self, state: &mut H) { - self.name.hash(state); + Hash::hash(&self.0 .0, state); } } impl PartialEq for Name { fn eq(&self, other: &Self) -> bool { - if self.hash != other.hash { + if self.0 .0.hash() != other.0 .0.hash() { // Makes the common case of two strings not been equal very fast return false; } - self.name.eq(&other.name) + self.0 .0.eq(&other.0 .0) } } @@ -217,7 +222,7 @@ impl PartialOrd for Name { impl Ord for Name { fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.name.cmp(&other.name) + self.0 .0.cmp(&other.0 .0) } } @@ -225,7 +230,7 @@ impl Deref for Name { type Target = str; fn deref(&self) -> &Self::Target { - self.name.as_ref() + self.as_str() } } diff --git a/crates/bevy_ecs/src/template.rs b/crates/bevy_ecs/src/template.rs new file mode 100644 index 0000000000000..c9f04b63c295f --- /dev/null +++ b/crates/bevy_ecs/src/template.rs @@ -0,0 +1,190 @@ +//! Functionality that relates to the [`Template`] trait. + +pub use bevy_ecs_macros::GetTemplate; + +use crate::{ + bundle::Bundle, + entity::{Entity, EntityPath}, + error::{BevyError, Result}, + world::EntityWorldMut, +}; +use alloc::{boxed::Box, vec, vec::Vec}; +use bevy_platform::collections::hash_map::Entry; +use bevy_utils::TypeIdMap; +use core::any::{Any, TypeId}; +use downcast_rs::{impl_downcast, Downcast}; +use variadics_please::all_tuples; + +/// A [`Template`] is something that, given a spawn context (target [`Entity`], [`World`](crate::world::World), etc), can produce a [`Template::Output`]. +pub trait Template { + /// The type of value produced by this [`Template`]. + type Output; + + /// Uses this template and the given `entity` context to produce a [`Template::Output`]. + fn build(&mut self, entity: &mut EntityWorldMut) -> Result; + + /// This is used to register information about the template, such as dependencies that should be loaded before it is instantiated. + #[inline] + fn register_data(&self, _data: &mut TemplateData) {} +} + +/// [`GetTemplate`] is implemented for types that can be produced by a specific, canonical [`Template`]. This creates a way to correlate to the [`Template`] using the +/// desired template output type. This is used by Bevy's scene system. +pub trait GetTemplate: Sized { + /// The [`Template`] for this type. + type Template: Template; +} + +macro_rules! template_impl { + ($($template: ident),*) => { + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such, the lints below may not always apply." + )] + impl<$($template: Template),*> Template for TemplateTuple<($($template,)*)> { + type Output = ($($template::Output,)*); + fn build(&mut self, _entity: &mut EntityWorldMut) -> Result { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($template,)*) = &mut self.0; + Ok(($($template.build(_entity)?,)*)) + } + + fn register_data(&self, _data: &mut TemplateData) { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($template,)*) = &self.0; + $($template.register_data(_data);)* + } + } + } +} + +/// A wrapper over a tuple of [`Template`] implementations, which also implements [`Template`]. This exists because [`Template`] cannot +/// be directly implemented for tuples of [`Template`] implementations. +pub struct TemplateTuple(pub T); + +all_tuples!(template_impl, 0, 12, T); + +impl Template for EntityPath<'static> { + type Output = Entity; + + fn build(&mut self, entity: &mut EntityWorldMut) -> Result { + Ok(entity.resolve_path(self)?) + } +} + +impl GetTemplate for Entity { + type Template = EntityPath<'static>; +} + +impl Template for T { + type Output = T; + + fn build(&mut self, _entity: &mut EntityWorldMut) -> Result { + Ok(self.clone()) + } +} + +impl GetTemplate for T { + type Template = T; +} + +/// A type-erased, object-safe, downcastable version of [`Template`]. +pub trait ErasedTemplate: Downcast + Send + Sync { + /// Applies this template to the given `entity`. + fn apply(&mut self, entity: &mut EntityWorldMut) -> Result<(), BevyError>; +} + +impl_downcast!(ErasedTemplate); + +impl + Send + Sync + 'static> ErasedTemplate for T { + fn apply(&mut self, entity: &mut EntityWorldMut) -> Result<(), BevyError> { + let bundle = self.build(entity)?; + entity.insert(bundle); + Ok(()) + } +} + +// TODO: Consider cutting this +/// A [`Template`] implementation that holds _either_ a [`Template`] value _or_ the [`Template::Output`] value. +pub enum TemplateField { + /// A [`Template`]. + Template(T), + /// A [`Template::Output`]. + Value(T::Output), +} + +impl Default for TemplateField { + fn default() -> Self { + Self::Template(::default()) + } +} + +impl> Template for TemplateField { + type Output = T::Output; + + fn build(&mut self, entity: &mut EntityWorldMut) -> Result { + Ok(match self { + TemplateField::Template(value) => value.build(entity)?, + TemplateField::Value(value) => value.clone(), + }) + } +} + +/// This is used by the [`GetTemplate`] derive to work around [this Rust limitation](https://github.com/rust-lang/rust/issues/86935). +/// A fix is implemented and on track for stabilization. If it is ever implemented, we can remove this. +pub type Wrapper = T; + +/// A [`Template`] driven by a function that returns an output. This is used to create "free floating" templates without +/// defining a new type. See [`template`] for usage. +pub struct FnTemplate Result, O>(pub F); + +impl Result, O> Template for FnTemplate { + type Output = O; + + fn build(&mut self, entity: &mut EntityWorldMut) -> Result { + (self.0)(entity) + } +} + +/// Returns a "free floating" template for a given `func`. This prevents the need to define a custom type for one-off templates. +pub fn template Result, O>(func: F) -> FnTemplate { + FnTemplate(func) +} + +/// Arbitrary data storage which can be used by [`Template`] implementations to register metadata such as asset dependencies. +#[derive(Default)] +pub struct TemplateData(TypeIdMap>); + +impl TemplateData { + /// Adds the `value` to this storage. This will be added to the back of a list of other values of the same type. + pub fn add(&mut self, value: T) { + match self.0.entry(TypeId::of::()) { + Entry::Occupied(mut entry) => { + entry + .get_mut() + .downcast_mut::>() + .unwrap() + .push(value); + } + Entry::Vacant(entry) => { + entry.insert(Box::new(vec![value])); + } + } + } + + /// Iterates over all stored values of the given type `T`. + pub fn iter(&self) -> impl Iterator { + if let Some(value) = self.0.get(&TypeId::of::()) { + let value = value.downcast_ref::>().unwrap(); + value.iter() + } else { + [].iter() + } + } +} diff --git a/crates/bevy_feathers/Cargo.toml b/crates/bevy_feathers/Cargo.toml index 07d883704ac73..ca82edaab1f5b 100644 --- a/crates/bevy_feathers/Cargo.toml +++ b/crates/bevy_feathers/Cargo.toml @@ -16,6 +16,7 @@ bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } bevy_core_widgets = { path = "../bevy_core_widgets", version = "0.17.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_scene2 = { path = "../bevy_scene2", version = "0.17.0-dev" } bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" } bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } diff --git a/crates/bevy_feathers/src/controls/button.rs b/crates/bevy_feathers/src/controls/button.rs index ad479f1ec5202..1bcf2732da1b2 100644 --- a/crates/bevy_feathers/src/controls/button.rs +++ b/crates/bevy_feathers/src/controls/button.rs @@ -1,29 +1,26 @@ use bevy_app::{Plugin, PreUpdate}; -use bevy_core_widgets::{Activate, Callback, CoreButton}; +use bevy_core_widgets::{Activate, CallbackTemplate, CoreButton}; use bevy_ecs::{ - bundle::Bundle, component::Component, entity::Entity, - hierarchy::{ChildOf, Children}, lifecycle::RemovedComponents, query::{Added, Changed, Has, Or}, schedule::IntoScheduleConfigs, - spawn::{SpawnRelated, SpawnableList}, system::{Commands, In, Query}, }; -use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; +use bevy_scene2::{prelude::*, template_value}; use bevy_ui::{AlignItems, InteractionDisabled, JustifyContent, Node, Pressed, UiRect, Val}; -use bevy_winit::cursor::CursorIcon; use crate::{ constants::{fonts, size}, font_styles::InheritableFont, - handle_or_path::HandleOrPath, rounded_corners::RoundedCorners, theme::{ThemeBackgroundColor, ThemeFontColor}, tokens, }; +use bevy_input_focus::tab_navigation::TabIndex; +use bevy_winit::cursor::CursorIcon; /// Color variants for buttons. This also functions as a component used by the dynamic styling /// system to identify which entities are buttons. @@ -45,46 +42,38 @@ 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, justify_content: JustifyContent::Center, align_items: AlignItems::Center, padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)), flex_grow: 1.0, - ..Default::default() - }, + } CoreButton { - on_activate: props.on_click, - }, - props.variant, - Hovered::default(), - CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - props.corners.to_border_radius(4.0), - ThemeBackgroundColor(tokens::BUTTON_BG), - ThemeFontColor(tokens::BUTTON_TEXT), + on_activate: {props.on_click.clone()}, + } + template_value(props.variant) + template_value(props.corners.to_border_radius(4.0)) + Hovered + // TODO: port CursonIcon to GetTemplate + // CursorIcon::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeBackgroundColor(tokens::BUTTON_BG) + ThemeFontColor(tokens::BUTTON_TEXT) InheritableFont { - font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font: fonts::REGULAR, font_size: 14.0, - }, - overrides, - Children::spawn(children), - ) + } + } } fn update_button_styles( diff --git a/crates/bevy_feathers/src/controls/checkbox.rs b/crates/bevy_feathers/src/controls/checkbox.rs index db37f82623c09..8f152398c3cf0 100644 --- a/crates/bevy_feathers/src/controls/checkbox.rs +++ b/crates/bevy_feathers/src/controls/checkbox.rs @@ -1,21 +1,19 @@ use bevy_app::{Plugin, PreUpdate}; -use bevy_core_widgets::{Callback, CoreCheckbox, ValueChange}; +use bevy_core_widgets::{CallbackTemplate, CoreCheckbox, ValueChange}; use bevy_ecs::{ - bundle::Bundle, - children, component::Component, entity::Entity, - hierarchy::{ChildOf, Children}, + hierarchy::Children, lifecycle::RemovedComponents, query::{Added, Changed, Has, Or, With}, schedule::IntoScheduleConfigs, - spawn::{Spawn, SpawnRelated, SpawnableList}, system::{Commands, In, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_math::Rot2; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_render::view::Visibility; +use bevy_scene2::prelude::*; use bevy_ui::{ AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, Node, PositionType, UiRect, UiTransform, Val, @@ -25,7 +23,6 @@ use bevy_winit::cursor::CursorIcon; use crate::{ constants::{fonts, size}, font_styles::InheritableFont, - handle_or_path::HandleOrPath, theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, tokens, }; @@ -34,7 +31,7 @@ use crate::{ #[derive(Default)] pub struct CheckboxProps { /// Change handler - pub on_change: Callback>>, + pub on_change: CallbackTemplate>>, } /// Marker for the checkbox frame (contains both checkbox and label) @@ -49,74 +46,61 @@ struct CheckboxOutline; #[derive(Component, Default, Clone)] struct CheckboxMark; -/// Template function to spawn a checkbox. +/// Checkbox scene function. /// /// # Arguments /// * `props` - construction properties for the checkbox. -/// * `overrides` - a bundle of components that are merged in with the normal checkbox components. -/// * `label` - the label of the checkbox. -pub fn checkbox + Send + Sync + 'static, B: Bundle>( - props: CheckboxProps, - overrides: B, - label: C, -) -> impl Bundle { - ( +pub fn checkbox(props: CheckboxProps) -> impl Scene { + bsn! { Node { display: Display::Flex, flex_direction: FlexDirection::Row, justify_content: JustifyContent::Start, align_items: AlignItems::Center, column_gap: Val::Px(4.0), - ..Default::default() - }, + } CoreCheckbox { - on_change: props.on_change, - }, - CheckboxFrame, - Hovered::default(), - CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - ThemeFontColor(tokens::CHECKBOX_TEXT), + on_change: {props.on_change.clone()}, + } + CheckboxFrame + Hovered + // TODO: port CursorIcon to GetTemplate + // CursorIcon::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeFontColor(tokens::CHECKBOX_TEXT) InheritableFont { - font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font: fonts::REGULAR, font_size: 14.0, - }, - overrides, - Children::spawn(( - Spawn(( + } + [( + Node { + width: size::CHECKBOX_SIZE, + height: size::CHECKBOX_SIZE, + border: UiRect::all(Val::Px(2.0)), + } + CheckboxOutline + BorderRadius::all(Val::Px(4.0)) + ThemeBackgroundColor(tokens::CHECKBOX_BG) + ThemeBorderColor(tokens::CHECKBOX_BORDER) + [( + // Cheesy checkmark: rotated node with L-shaped border. Node { - width: size::CHECKBOX_SIZE, - height: size::CHECKBOX_SIZE, - border: UiRect::all(Val::Px(2.0)), - ..Default::default() - }, - CheckboxOutline, - BorderRadius::all(Val::Px(4.0)), - ThemeBackgroundColor(tokens::CHECKBOX_BG), - ThemeBorderColor(tokens::CHECKBOX_BORDER), - children![( - // Cheesy checkmark: rotated node with L-shaped border. - Node { - position_type: PositionType::Absolute, - left: Val::Px(4.0), - top: Val::Px(0.0), - width: Val::Px(6.), - height: Val::Px(11.), - border: UiRect { - bottom: Val::Px(2.0), - right: Val::Px(2.0), - ..Default::default() - }, - ..Default::default() + position_type: PositionType::Absolute, + left: Val::Px(4.0), + top: Val::Px(0.0), + width: Val::Px(6.), + height: Val::Px(11.), + border: UiRect { + bottom: Val::Px(2.0), + right: Val::Px(2.0), }, - UiTransform::from_rotation(Rot2::FRAC_PI_4), - CheckboxMark, - ThemeBorderColor(tokens::CHECKBOX_MARK), - )], - )), - label, - )), - ) + } + UiTransform::from_rotation(Rot2::FRAC_PI_4) + CheckboxMark + ThemeBorderColor(tokens::CHECKBOX_MARK) + )] + )] + } } fn update_checkbox_styles( diff --git a/crates/bevy_feathers/src/controls/radio.rs b/crates/bevy_feathers/src/controls/radio.rs index a08ffcfa8d136..0f9b7ec9582ca 100644 --- a/crates/bevy_feathers/src/controls/radio.rs +++ b/crates/bevy_feathers/src/controls/radio.rs @@ -1,20 +1,18 @@ use bevy_app::{Plugin, PreUpdate}; use bevy_core_widgets::CoreRadio; use bevy_ecs::{ - bundle::Bundle, - children, component::Component, entity::Entity, - hierarchy::{ChildOf, Children}, + hierarchy::Children, lifecycle::RemovedComponents, query::{Added, Changed, Has, Or, With}, schedule::IntoScheduleConfigs, - spawn::{Spawn, SpawnRelated, SpawnableList}, system::{Commands, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_render::view::Visibility; +use bevy_scene2::prelude::*; use bevy_ui::{ AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, Node, UiRect, Val, @@ -24,7 +22,6 @@ use bevy_winit::cursor::CursorIcon; use crate::{ constants::{fonts, size}, font_styles::InheritableFont, - handle_or_path::HandleOrPath, theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, tokens, }; @@ -37,64 +34,50 @@ struct RadioOutline; #[derive(Component, Default, Clone)] struct RadioMark; -/// Template function to spawn a radio. -/// -/// # Arguments -/// * `props` - construction properties for the radio. -/// * `overrides` - a bundle of components that are merged in with the normal radio components. -/// * `label` - the label of the radio. -pub fn radio + Send + Sync + 'static, B: Bundle>( - overrides: B, - label: C, -) -> impl Bundle { - ( +/// Radio scene function. +pub fn radio() -> impl Scene { + bsn! { Node { display: Display::Flex, flex_direction: FlexDirection::Row, justify_content: JustifyContent::Start, align_items: AlignItems::Center, column_gap: Val::Px(4.0), - ..Default::default() - }, - CoreRadio, - Hovered::default(), - CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - ThemeFontColor(tokens::RADIO_TEXT), + } + CoreRadio + Hovered + // TODO: port CursorIcon to GetTemplate + // CursorIcon::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeFontColor(tokens::RADIO_TEXT) InheritableFont { - font: HandleOrPath::Path(fonts::REGULAR.to_owned()), + font: fonts::REGULAR, font_size: 14.0, - }, - overrides, - Children::spawn(( - Spawn(( + } + [( + Node { + display: Display::Flex, + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + width: size::RADIO_SIZE, + height: size::RADIO_SIZE, + border: UiRect::all(Val::Px(2.0)), + } + RadioOutline + BorderRadius::MAX + ThemeBorderColor(tokens::RADIO_BORDER) + [( + // Cheesy checkmark: rotated node with L-shaped border. Node { - display: Display::Flex, - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - width: size::RADIO_SIZE, - height: size::RADIO_SIZE, - border: UiRect::all(Val::Px(2.0)), - ..Default::default() - }, - RadioOutline, - BorderRadius::MAX, - ThemeBorderColor(tokens::RADIO_BORDER), - children![( - // Cheesy checkmark: rotated node with L-shaped border. - Node { - width: Val::Px(8.), - height: Val::Px(8.), - ..Default::default() - }, - BorderRadius::MAX, - RadioMark, - ThemeBackgroundColor(tokens::RADIO_MARK), - )], - )), - label, - )), - ) + width: Val::Px(8.), + height: Val::Px(8.), + } + BorderRadius::MAX + RadioMark + ThemeBackgroundColor(tokens::RADIO_MARK) + )] + )] + } } fn update_radio_styles( diff --git a/crates/bevy_feathers/src/controls/slider.rs b/crates/bevy_feathers/src/controls/slider.rs index 228801b85cf3e..915da874b3358 100644 --- a/crates/bevy_feathers/src/controls/slider.rs +++ b/crates/bevy_feathers/src/controls/slider.rs @@ -2,21 +2,21 @@ use core::f32::consts::PI; use bevy_app::{Plugin, PreUpdate}; use bevy_color::Color; -use bevy_core_widgets::{Callback, CoreSlider, SliderRange, SliderValue, TrackClick, ValueChange}; +use bevy_core_widgets::{ + CallbackTemplate, CoreSlider, SliderRange, SliderValue, TrackClick, ValueChange, +}; use bevy_ecs::{ - bundle::Bundle, - children, component::Component, entity::Entity, hierarchy::Children, lifecycle::RemovedComponents, query::{Added, Changed, Has, Or, Spawned, With}, schedule::IntoScheduleConfigs, - spawn::SpawnRelated, system::{In, Query, Res}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::PickingSystems; +use bevy_scene2::{prelude::*, template_value}; use bevy_ui::{ widget::Text, AlignItems, BackgroundGradient, ColorStop, Display, FlexDirection, Gradient, InteractionDisabled, InterpolationColorSpace, JustifyContent, LinearGradient, Node, UiRect, @@ -27,7 +27,6 @@ use bevy_winit::cursor::CursorIcon; use crate::{ constants::{fonts, size}, font_styles::InheritableFont, - handle_or_path::HandleOrPath, rounded_corners::RoundedCorners, theme::{ThemeFontColor, ThemedText, UiTheme}, tokens, @@ -42,7 +41,7 @@ pub struct SliderProps { /// Slider maximum value pub max: f32, /// On-change handler - pub on_change: Callback>>, + pub on_change: CallbackTemplate>>, } impl Default for SliderProps { @@ -51,47 +50,45 @@ impl Default for SliderProps { value: 0.0, min: 0.0, max: 1.0, - on_change: Callback::Ignore, + on_change: CallbackTemplate::Ignore, } } } #[derive(Component, Default, Clone)] -#[require(CoreSlider)] struct SliderStyle; /// Marker for the text #[derive(Component, Default, Clone)] struct SliderValueText; -/// Spawn a new slider widget. +/// Slider scene function. /// /// # Arguments /// /// * `props` - construction properties for the slider. -/// * `overrides` - a bundle of components that are merged in with the normal slider components. -pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { - ( +pub fn slider(props: SliderProps) -> impl Scene { + bsn! { Node { height: size::ROW_HEIGHT, justify_content: JustifyContent::Center, align_items: AlignItems::Center, padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)), flex_grow: 1.0, - ..Default::default() - }, + } CoreSlider { - on_change: props.on_change, + on_change: {props.on_change.clone()}, track_click: TrackClick::Drag, - }, - SliderStyle, - SliderValue(props.value), - SliderRange::new(props.min, props.max), - CursorIcon::System(bevy_window::SystemCursorIcon::EwResize), - TabIndex(0), - RoundedCorners::All.to_border_radius(6.0), + } + SliderStyle + SliderValue({props.value}) + SliderRange::new(props.min, props.max) + // TODO: port CursorIcon to GetTemplate + // CursorIcon::System(bevy_window::SystemCursorIcon::EwResize) + TabIndex(0) + template_value(RoundedCorners::All.to_border_radius(6.0)) // Use a gradient to draw the moving bar - BackgroundGradient(vec![Gradient::Linear(LinearGradient { + BackgroundGradient({vec![Gradient::Linear(LinearGradient { angle: PI * 0.5, stops: vec![ ColorStop::new(Color::NONE, Val::Percent(0.)), @@ -100,25 +97,23 @@ pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { ColorStop::new(Color::NONE, Val::Percent(100.)), ], color_space: InterpolationColorSpace::Srgb, - })]), - overrides, - children![( + })]}) + [( // Text container Node { display: Display::Flex, flex_direction: FlexDirection::Row, align_items: AlignItems::Center, justify_content: JustifyContent::Center, - ..Default::default() - }, - ThemeFontColor(tokens::SLIDER_TEXT), + } + ThemeFontColor(tokens::SLIDER_TEXT) InheritableFont { - font: HandleOrPath::Path(fonts::MONO.to_owned()), + font: fonts::MONO, font_size: 12.0, - }, - children![(Text::new("10.0"), ThemedText, SliderValueText,)], - )], - ) + } + [(Text::new("10.0") ThemedText SliderValueText)] + )] + } } fn update_slider_colors( diff --git a/crates/bevy_feathers/src/controls/toggle_switch.rs b/crates/bevy_feathers/src/controls/toggle_switch.rs index e3437a829d6a5..2d43625e48cf0 100644 --- a/crates/bevy_feathers/src/controls/toggle_switch.rs +++ b/crates/bevy_feathers/src/controls/toggle_switch.rs @@ -1,24 +1,21 @@ use accesskit::Role; use bevy_a11y::AccessibilityNode; use bevy_app::{Plugin, PreUpdate}; -use bevy_core_widgets::{Callback, CoreCheckbox, ValueChange}; +use bevy_core_widgets::{Callback, CallbackTemplate, CoreCheckbox, ValueChange}; use bevy_ecs::{ - bundle::Bundle, - children, component::Component, entity::Entity, hierarchy::Children, lifecycle::RemovedComponents, query::{Added, Changed, Has, Or, With}, schedule::IntoScheduleConfigs, - spawn::SpawnRelated, system::{Commands, In, Query}, world::Mut, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; +use bevy_scene2::prelude::*; use bevy_ui::{BorderRadius, Checked, InteractionDisabled, Node, PositionType, UiRect, Val}; -use bevy_winit::cursor::CursorIcon; use crate::{ constants::size, @@ -30,7 +27,7 @@ use crate::{ #[derive(Default)] pub struct ToggleSwitchProps { /// Change handler - pub on_change: Callback>>, + pub on_change: CallbackTemplate>>, } /// Marker for the toggle switch outline @@ -41,45 +38,42 @@ struct ToggleSwitchOutline; #[derive(Component, Default, Clone)] struct ToggleSwitchSlide; -/// Template function to spawn a toggle switch. +/// Toggle switch scene function. /// /// # Arguments /// * `props` - construction properties for the toggle switch. -/// * `overrides` - a bundle of components that are merged in with the normal toggle switch components. -pub fn toggle_switch(props: ToggleSwitchProps, overrides: B) -> impl Bundle { - ( +pub fn toggle_switch(props: ToggleSwitchProps) -> impl Scene { + bsn! { Node { width: size::TOGGLE_WIDTH, height: size::TOGGLE_HEIGHT, border: UiRect::all(Val::Px(2.0)), - ..Default::default() - }, + } CoreCheckbox { - on_change: props.on_change, - }, - ToggleSwitchOutline, - BorderRadius::all(Val::Px(5.0)), - ThemeBackgroundColor(tokens::SWITCH_BG), - ThemeBorderColor(tokens::SWITCH_BORDER), - AccessibilityNode(accesskit::Node::new(Role::Switch)), - Hovered::default(), - CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), - TabIndex(0), - overrides, - children![( + on_change: {props.on_change.clone()}, + } + ToggleSwitchOutline + BorderRadius::all(Val::Px(5.0)) + ThemeBackgroundColor(tokens::SWITCH_BG) + ThemeBorderColor(tokens::SWITCH_BORDER) + AccessibilityNode(accesskit::Node::new(Role::Switch)) + Hovered + // TODO: port CursorIcon to GetTemplate + // CursorIcon::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + [( Node { position_type: PositionType::Absolute, left: Val::Percent(0.), top: Val::Px(0.), bottom: Val::Px(0.), width: Val::Percent(50.), - ..Default::default() - }, - BorderRadius::all(Val::Px(3.0)), - ToggleSwitchSlide, - ThemeBackgroundColor(tokens::SWITCH_SLIDE), - )], - ) + } + BorderRadius::all(Val::Px(3.0)) + ToggleSwitchSlide + ThemeBackgroundColor(tokens::SWITCH_SLIDE) + )] + } } fn update_switch_styles( diff --git a/crates/bevy_feathers/src/font_styles.rs b/crates/bevy_feathers/src/font_styles.rs index 6de064dd39248..9d45c6a40278f 100644 --- a/crates/bevy_feathers/src/font_styles.rs +++ b/crates/bevy_feathers/src/font_styles.rs @@ -1,62 +1,37 @@ //! A framework for inheritable font styles. use bevy_app::Propagate; -use bevy_asset::{AssetServer, Handle}; +use bevy_asset::Handle; use bevy_ecs::{ component::Component, lifecycle::Insert, observer::On, - system::{Commands, Query, Res}, + system::{Commands, Query}, + template::GetTemplate, }; use bevy_text::{Font, TextFont}; -use crate::handle_or_path::HandleOrPath; - /// A component which, when inserted on an entity, will load the given font and propagate it /// downward to any child text entity that has the [`ThemedText`](crate::theme::ThemedText) marker. -#[derive(Component, Default, Clone, Debug)] +#[derive(Component, Clone, Debug, GetTemplate)] pub struct InheritableFont { /// The font handle or path. - pub font: HandleOrPath, + pub font: Handle, /// The desired font size. pub font_size: f32, } -impl InheritableFont { - /// Create a new `InheritableFont` from a handle. - pub fn from_handle(handle: Handle) -> Self { - Self { - font: HandleOrPath::Handle(handle), - font_size: 16.0, - } - } - - /// Create a new `InheritableFont` from a path. - pub fn from_path(path: &str) -> Self { - Self { - font: HandleOrPath::Path(path.to_string()), - font_size: 16.0, - } - } -} - /// An observer which looks for changes to the `InheritableFont` component on an entity, and /// propagates downward the font to all participating text entities. pub(crate) fn on_changed_font( ev: On, font_style: Query<&InheritableFont>, - assets: Res, mut commands: Commands, ) { - if let Ok(style) = font_style.get(ev.target()) { - if let Some(font) = match style.font { - HandleOrPath::Handle(ref h) => Some(h.clone()), - HandleOrPath::Path(ref p) => Some(assets.load::(p)), - } { - commands.entity(ev.target()).insert(Propagate(TextFont { - font, - font_size: style.font_size, - ..Default::default() - })); - } + if let Ok(inheritable_font) = font_style.get(ev.target()) { + commands.entity(ev.target()).insert(Propagate(TextFont { + font: inheritable_font.font.clone(), + font_size: inheritable_font.font_size, + ..Default::default() + })); } } diff --git a/crates/bevy_feathers/src/theme.rs b/crates/bevy_feathers/src/theme.rs index 9969b54846667..56f6cd3192936 100644 --- a/crates/bevy_feathers/src/theme.rs +++ b/crates/bevy_feathers/src/theme.rs @@ -49,26 +49,26 @@ impl UiTheme { } /// Component which causes the background color of an entity to be set based on a theme color. -#[derive(Component, Clone, Copy)] +#[derive(Component, Clone, Copy, Default)] #[require(BackgroundColor)] #[component(immutable)] pub struct ThemeBackgroundColor(pub &'static str); /// Component which causes the border color of an entity to be set based on a theme color. /// Only supports setting all borders to the same color. -#[derive(Component, Clone, Copy)] +#[derive(Component, Clone, Copy, Default)] #[require(BorderColor)] #[component(immutable)] pub struct ThemeBorderColor(pub &'static str); /// Component which causes the inherited text color of an entity to be set based on a theme color. -#[derive(Component, Clone, Copy)] +#[derive(Component, Clone, Copy, Default)] #[component(immutable)] pub struct ThemeFontColor(pub &'static str); /// A marker component that is used to indicate that the text entity wants to opt-in to using /// inherited text styles. -#[derive(Component)] +#[derive(Component, Default, Clone)] pub struct ThemedText; pub(crate) fn update_theme( diff --git a/crates/bevy_gizmos/src/retained.rs b/crates/bevy_gizmos/src/retained.rs index 4cc75f236da13..97e51c41322c6 100644 --- a/crates/bevy_gizmos/src/retained.rs +++ b/crates/bevy_gizmos/src/retained.rs @@ -4,7 +4,7 @@ use core::ops::{Deref, DerefMut}; use bevy_asset::Handle; use bevy_ecs::{component::Component, reflect::ReflectComponent}; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_reflect::Reflect; use bevy_transform::components::Transform; #[cfg(feature = "bevy_render")] @@ -72,8 +72,8 @@ impl DerefMut for GizmoAsset { /// ``` /// /// [`Gizmos`]: crate::gizmos::Gizmos -#[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component, Clone, Default)] +#[derive(Component, Clone, Debug, Reflect)] +#[reflect(Component, Clone)] #[require(Transform)] pub struct Gizmo { /// The handle to the gizmo to draw. @@ -95,6 +95,16 @@ pub struct Gizmo { pub depth_bias: f32, } +impl Default for Gizmo { + fn default() -> Self { + Self { + handle: Handle::default(), + line_config: Default::default(), + depth_bias: Default::default(), + } + } +} + #[cfg(feature = "bevy_render")] pub(crate) fn extract_linegizmos( mut commands: Commands, diff --git a/crates/bevy_image/src/texture_atlas.rs b/crates/bevy_image/src/texture_atlas.rs index 67e1b203170a0..111d912d5c298 100644 --- a/crates/bevy_image/src/texture_atlas.rs +++ b/crates/bevy_image/src/texture_atlas.rs @@ -204,7 +204,7 @@ impl TextureAtlasLayout { /// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs) /// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs) /// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs) -#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -217,6 +217,15 @@ pub struct TextureAtlas { pub index: usize, } +impl Default for TextureAtlas { + fn default() -> Self { + Self { + layout: Handle::default(), + index: Default::default(), + } + } +} + impl TextureAtlas { /// Retrieves the current texture [`URect`] of the sprite sheet according to the section `index` pub fn texture_rect(&self, texture_atlases: &Assets) -> Option { diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index e591803751f7d..ca99a50d39389 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -443,6 +443,7 @@ bevy_picking = { path = "../bevy_picking", optional = true, version = "0.17.0-de bevy_remote = { path = "../bevy_remote", optional = true, version = "0.17.0-dev" } bevy_render = { path = "../bevy_render", optional = true, version = "0.17.0-dev" } bevy_scene = { path = "../bevy_scene", optional = true, version = "0.17.0-dev" } +bevy_scene2 = { path = "../bevy_scene2", optional = true, version = "0.17.0-dev" } bevy_solari = { path = "../bevy_solari", optional = true, version = "0.17.0-dev" } bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.17.0-dev" } bevy_state = { path = "../bevy_state", optional = true, version = "0.17.0-dev", default-features = false, features = [ diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index cdb59921dcc74..d52856cb16b21 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -25,6 +25,8 @@ plugin_group! { bevy_asset:::AssetPlugin, #[cfg(feature = "bevy_scene")] bevy_scene:::ScenePlugin, + #[cfg(feature = "bevy_scene2")] + bevy_scene2:::ScenePlugin, #[cfg(feature = "bevy_winit")] bevy_winit:::WinitPlugin, #[cfg(feature = "bevy_render")] diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 4f965e603a76f..292d073729c65 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -70,6 +70,8 @@ pub use bevy_remote as remote; pub use bevy_render as render; #[cfg(feature = "bevy_scene")] pub use bevy_scene as scene; +#[cfg(feature = "bevy_scene2")] +pub use bevy_scene2 as scene2; #[cfg(feature = "bevy_solari")] pub use bevy_solari as solari; #[cfg(feature = "bevy_sprite")] diff --git a/crates/bevy_mesh/src/components.rs b/crates/bevy_mesh/src/components.rs index cff5eab7e477f..635701fd8bba2 100644 --- a/crates/bevy_mesh/src/components.rs +++ b/crates/bevy_mesh/src/components.rs @@ -37,11 +37,17 @@ use derive_more::derive::From; /// )); /// } /// ``` -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Clone, PartialEq)] #[require(Transform)] pub struct Mesh2d(pub Handle); +impl Default for Mesh2d { + fn default() -> Self { + Self(Handle::default()) + } +} + impl From for AssetId { fn from(mesh: Mesh2d) -> Self { mesh.id() @@ -92,11 +98,17 @@ impl AsAssetId for Mesh2d { /// )); /// } /// ``` -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Clone, PartialEq)] #[require(Transform)] pub struct Mesh3d(pub Handle); +impl Default for Mesh3d { + fn default() -> Self { + Self(Handle::default()) + } +} + impl From for AssetId { fn from(mesh: Mesh3d) -> Self { mesh.id() diff --git a/crates/bevy_mesh/src/skinning.rs b/crates/bevy_mesh/src/skinning.rs index 53b93f9ff2507..5531288102748 100644 --- a/crates/bevy_mesh/src/skinning.rs +++ b/crates/bevy_mesh/src/skinning.rs @@ -4,14 +4,23 @@ use bevy_math::Mat4; use bevy_reflect::prelude::*; use core::ops::Deref; -#[derive(Component, Debug, Default, Clone, Reflect)] -#[reflect(Component, Default, Debug, Clone)] +#[derive(Component, Debug, Clone, Reflect)] +#[reflect(Component, Debug, Clone)] pub struct SkinnedMesh { pub inverse_bindposes: Handle, #[entities] pub joints: Vec, } +impl Default for SkinnedMesh { + fn default() -> Self { + Self { + inverse_bindposes: Handle::default(), + joints: Default::default(), + } + } +} + impl AsAssetId for SkinnedMesh { type Asset = SkinnedMeshInverseBindposes; diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index 682fac09c053e..3bd6f3b96f2ca 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -325,7 +325,7 @@ pub(crate) fn pack_lightmap_uv_rect(maybe_rect: Option) -> UVec2 { impl Default for Lightmap { fn default() -> Self { Self { - image: Default::default(), + image: Handle::default(), uv_rect: Rect::new(0.0, 0.0, 1.0, 1.0), bicubic_sampling: false, } diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index ad280e054f67d..10e5c83c0139d 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -452,10 +452,16 @@ pub struct RenderWireframeMaterial { pub color: [f32; 4], } -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq)] #[reflect(Component, Default, Clone, PartialEq)] pub struct Mesh3dWireframe(pub Handle); +impl Default for Mesh3dWireframe { + fn default() -> Self { + Self(Handle::default()) + } +} + impl AsAssetId for Mesh3dWireframe { type Asset = WireframeMaterial; diff --git a/crates/bevy_platform/src/hash.rs b/crates/bevy_platform/src/hash.rs index 3b1a836ecf83d..5cd4d177fcd90 100644 --- a/crates/bevy_platform/src/hash.rs +++ b/crates/bevy_platform/src/hash.rs @@ -104,6 +104,13 @@ impl Clone for Hashed { } } +impl From for Hashed { + #[inline] + fn from(value: V) -> Self { + Self::new(value) + } +} + impl Copy for Hashed {} impl Eq for Hashed {} diff --git a/crates/bevy_reflect/src/impls/bevy_platform/hash.rs b/crates/bevy_reflect/src/impls/bevy_platform/hash.rs index c2a38dd5f31bc..3fe85255a0567 100644 --- a/crates/bevy_reflect/src/impls/bevy_platform/hash.rs +++ b/crates/bevy_reflect/src/impls/bevy_platform/hash.rs @@ -1,5 +1,7 @@ -use bevy_reflect_derive::impl_type_path; +use bevy_reflect_derive::{impl_reflect_opaque, impl_type_path}; impl_type_path!(::bevy_platform::hash::NoOpHash); impl_type_path!(::bevy_platform::hash::FixedHasher); impl_type_path!(::bevy_platform::hash::PassHash); + +impl_reflect_opaque!(::bevy_platform::hash::Hashed()); diff --git a/crates/bevy_render/src/render_resource/pipeline.rs b/crates/bevy_render/src/render_resource/pipeline.rs index e94cf27cd32c8..0e46507083366 100644 --- a/crates/bevy_render/src/render_resource/pipeline.rs +++ b/crates/bevy_render/src/render_resource/pipeline.rs @@ -112,7 +112,7 @@ pub struct RenderPipelineDescriptor { pub zero_initialize_workgroup_memory: bool, } -#[derive(Clone, Debug, Eq, PartialEq, Default)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct VertexState { /// The compiled shader module for this stage. pub shader: Handle, @@ -124,8 +124,19 @@ pub struct VertexState { pub buffers: Vec, } +impl Default for VertexState { + fn default() -> Self { + Self { + shader: Handle::default(), + shader_defs: Default::default(), + entry_point: Default::default(), + buffers: Default::default(), + } + } +} + /// Describes the fragment process in a render pipeline. -#[derive(Clone, Debug, PartialEq, Eq, Default)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct FragmentState { /// The compiled shader module for this stage. pub shader: Handle, @@ -137,8 +148,19 @@ pub struct FragmentState { pub targets: Vec>, } +impl Default for FragmentState { + fn default() -> Self { + Self { + shader: Handle::default(), + shader_defs: Default::default(), + entry_point: Default::default(), + targets: Default::default(), + } + } +} + /// Describes a compute pipeline. -#[derive(Clone, Debug, PartialEq, Eq, Default)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ComputePipelineDescriptor { pub label: Option>, pub layout: Vec, @@ -153,3 +175,17 @@ pub struct ComputePipelineDescriptor { /// If this is false, reading from workgroup variables before writing to them will result in garbage values. pub zero_initialize_workgroup_memory: bool, } + +impl Default for ComputePipelineDescriptor { + fn default() -> Self { + Self { + shader: Handle::default(), + label: Default::default(), + layout: Default::default(), + push_constant_ranges: Default::default(), + shader_defs: Default::default(), + entry_point: Default::default(), + zero_initialize_workgroup_memory: Default::default(), + } + } +} diff --git a/crates/bevy_scene/src/components.rs b/crates/bevy_scene/src/components.rs index d4d42c3a1c98c..0c2d98962ce4b 100644 --- a/crates/bevy_scene/src/components.rs +++ b/crates/bevy_scene/src/components.rs @@ -12,16 +12,28 @@ use crate::{DynamicScene, Scene}; /// Adding this component will spawn the scene as a child of that entity. /// Once it's spawned, the entity will have a [`SceneInstance`](crate::SceneInstance) component. -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Debug, PartialEq, Clone)] #[require(Transform)] #[cfg_attr(feature = "bevy_render", require(Visibility))] pub struct SceneRoot(pub Handle); +impl Default for SceneRoot { + fn default() -> Self { + Self(Handle::default()) + } +} + /// Adding this component will spawn the scene as a child of that entity. /// Once it's spawned, the entity will have a [`SceneInstance`](crate::SceneInstance) component. -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, From)] #[reflect(Component, Default, Debug, PartialEq, Clone)] #[require(Transform)] #[cfg_attr(feature = "bevy_render", require(Visibility))] pub struct DynamicSceneRoot(pub Handle); + +impl Default for DynamicSceneRoot { + fn default() -> Self { + Self(Handle::default()) + } +} diff --git a/crates/bevy_scene2/Cargo.toml b/crates/bevy_scene2/Cargo.toml new file mode 100644 index 0000000000000..8363bab9a6dbd --- /dev/null +++ b/crates/bevy_scene2/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "bevy_scene2" +version = "0.17.0-dev" +edition = "2024" + +[dependencies] +bevy_scene2_macros = { path = "macros", version = "0.17.0-dev" } + +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } + +variadics_please = "1.0" + +[lints] +workspace = true diff --git a/crates/bevy_scene2/macros/Cargo.toml b/crates/bevy_scene2/macros/Cargo.toml new file mode 100644 index 0000000000000..f884eda88d0e9 --- /dev/null +++ b/crates/bevy_scene2/macros/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "bevy_scene2_macros" +version = "0.17.0-dev" +edition = "2024" +description = "Derive implementations for bevy_scene" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[lib] +proc-macro = true + +[dependencies] +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } + +syn = { version = "2.0", features = ["full", "extra-traits"] } +proc-macro2 = "1.0" +quote = "1.0" diff --git a/crates/bevy_scene2/macros/src/bsn/codegen.rs b/crates/bevy_scene2/macros/src/bsn/codegen.rs new file mode 100644 index 0000000000000..0b29c88c0247a --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/codegen.rs @@ -0,0 +1,371 @@ +use crate::bsn::types::{ + Bsn, BsnConstructor, BsnEntry, BsnFields, BsnInheritedScene, BsnRelatedSceneList, BsnRoot, BsnSceneListItem, BsnSceneListItems, BsnType, BsnValue +}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::{Ident, Index, Lit, Member, Path}; + +impl BsnRoot { + pub fn to_tokens(&self, bevy_scene: &Path, bevy_ecs: &Path, bevy_asset: &Path) -> TokenStream { + self.0.to_tokens(bevy_scene, bevy_ecs, bevy_asset) + } +} + +impl Bsn { + pub fn to_tokens(&self, bevy_scene: &Path, bevy_ecs: &Path, bevy_asset: &Path) -> TokenStream { + let mut entries = Vec::with_capacity(self.entries.len()); + for bsn_entry in &self.entries { + entries.push(match bsn_entry { + BsnEntry::TemplatePatch(bsn_type) => { + let mut assignments = Vec::new(); + bsn_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + &mut assignments, + true, + &[Member::Named(Ident::new( + "value", + proc_macro2::Span::call_site(), + ))], + true, + ); + let path = &bsn_type.path; + quote! { + <#path as #bevy_scene::PatchTemplate>::patch_template(move |value| { + #(#assignments)* + }) + } + } + BsnEntry::GetTemplatePatch(bsn_type) => { + let mut assignments = Vec::new(); + bsn_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + &mut assignments, + true, + &[Member::Named(Ident::new( + "value", + proc_macro2::Span::call_site(), + ))], + true, + ); + let path = &bsn_type.path; + quote! { + <#path as #bevy_scene::PatchGetTemplate>::patch(move |value| { + #(#assignments)* + }) + } + } + BsnEntry::TemplateConst{ type_path, const_ident} => { + quote!{ + <#type_path as #bevy_scene::PatchTemplate>::patch_template( + move |value| { + *value = #type_path::#const_ident; + }, + ) + } + } + BsnEntry::SceneExpression(block) => { + quote!{#block} + } + BsnEntry::TemplateConstructor(BsnConstructor {type_path, function, args})=> { + quote! { + <#type_path as #bevy_scene::PatchTemplate>::patch_template( + move |value| { + *value = #type_path::#function(#args); + }, + ) + } + } + BsnEntry::GetTemplateConstructor(BsnConstructor {type_path, function, args})=> { + quote! { + <#type_path as #bevy_scene::PatchGetTemplate>::patch( + move |value| { + *value = <#type_path as #bevy_ecs::template::GetTemplate>::Template::#function(#args); + } + ) + } + } + BsnEntry::ChildrenSceneList(scene_list) => { + let scenes = scene_list.0.to_tokens(bevy_scene, bevy_ecs, bevy_asset); + quote! { + #bevy_scene::RelatedScenes::<#bevy_ecs::hierarchy::ChildOf, _>::new(#scenes) + } + } + BsnEntry::RelatedSceneList(BsnRelatedSceneList { scene_list, relationship_path }) => { + let scenes = scene_list.0.to_tokens(bevy_scene, bevy_ecs, bevy_asset); + quote! { + #bevy_scene::RelatedScenes::<<#relationship_path as #bevy_ecs::relationship::RelationshipTarget>::Relationship, _>::new( + #scenes + ) + } + } + BsnEntry::InheritedScene(inherited_scene) => match inherited_scene { + BsnInheritedScene::Asset(lit_str) => { + quote!{#bevy_scene::InheritSceneAsset::from(#lit_str)} + }, + BsnInheritedScene::Fn{ function, args}=> quote!{#bevy_scene::InheritScene(#function(#args))}, + } + BsnEntry::Name(ident) => { + let name = ident.to_string(); + quote! { + <#bevy_ecs::name::Name as PatchGetTemplate>::patch( + move |value| { + *value = Name(#name.into()); + } + ) + } + } + + }); + } + + quote! {(#(#entries,)*)} + } +} + +macro_rules! field_value_type { + () => { + BsnValue::Expr(_) + | BsnValue::Closure(_) + | BsnValue::Ident(_) + | BsnValue::Lit(_) + | BsnValue::Tuple(_) + }; +} + +impl BsnType { + fn to_patch_tokens( + &self, + bevy_ecs: &Path, + bevy_scene: &Path, + assignments: &mut Vec, + is_root_template: bool, + field_path: &[Member], + is_path_ref: bool, + ) { + let path = &self.path; + if !is_root_template { + assignments.push(quote! {#bevy_scene::touch_type::<#path>();}); + } + let maybe_deref = is_path_ref.then(|| quote!{*}); + let maybe_borrow_mut = (!is_path_ref).then(|| quote!{&mut}); + if let Some(variant) = &self.enum_variant { + let variant_name_lower = variant.to_string().to_lowercase(); + let variant_default_name = format_ident!("default_{}", variant_name_lower); + match &self.fields { + BsnFields::Named(fields) => { + let field_assignments = fields.iter().map(|f| { + let name = &f.name; + let value = &f.value; + if let Some(BsnValue::Type(bsn_type)) = &value { + if bsn_type.enum_variant.is_some() { + quote!{*#name = #bsn_type;} + } else { + let mut type_assignments = Vec::new(); + bsn_type.to_patch_tokens(bevy_ecs, bevy_scene, &mut type_assignments, false, &[Member::Named(name.clone())], true); + quote!{#(#type_assignments)*} + } + } else { + quote!{*#name = #value;} + } + }); + let field_names = fields.iter().map(|f| &f.name); + assignments.push(quote! { + if !matches!(#(#field_path).*, #bevy_ecs::template::Wrapper::<<#path as #bevy_ecs::template::GetTemplate>::Template>::#variant { .. }) { + #maybe_deref #(#field_path).* = #bevy_ecs::template::Wrapper::<<#path as #bevy_ecs::template::GetTemplate>::Template>::#variant_default_name(); + } + if let #bevy_ecs::template::Wrapper::<<#path as #bevy_ecs::template::GetTemplate>::Template>::#variant { #(#field_names, )*.. } = #maybe_borrow_mut #(#field_path).* { + #(#field_assignments)* + } + }) + } + BsnFields::Tuple(fields) => { + // root template enums produce per-field "patches", at the cost of requiring the EnumDefaults pattern + let field_assignments = fields.iter().enumerate().map(|(index, f)| { + let name = format_ident!("t{}", index); + let value = &f.value; + if let BsnValue::Type(bsn_type) = &value { + if bsn_type.enum_variant.is_some() { + quote!{*#name = #bsn_type;} + } else { + let mut type_assignments = Vec::new(); + bsn_type.to_patch_tokens(bevy_ecs, bevy_scene, &mut type_assignments, false, &[Member::Named(name.clone())], true); + quote!{#(#type_assignments)*} + } + } else { + quote!{*#name = #value;} + } + }); + let field_names = fields.iter().enumerate().map(|(index, _)| format_ident!("t{}", index)); + assignments.push(quote! { + if !matches!(#(#field_path).*, #bevy_ecs::template::Wrapper::<<#path as #bevy_ecs::template::GetTemplate>::Template>::#variant(..)) { + #maybe_deref #(#field_path).* = #bevy_ecs::template::Wrapper::<<#path as #bevy_ecs::template::GetTemplate>::Template>::#variant_default_name(); + } + if let #bevy_ecs::template::Wrapper::<<#path as #bevy_ecs::template::GetTemplate>::Template>::#variant (#(#field_names, )*.. ) = #maybe_borrow_mut #(#field_path).* { + #(#field_assignments)* + } + }) + } + } + } else { + match &self.fields { + BsnFields::Named(fields) => { + for field in fields { + let field_name = &field.name; + let field_value = &field.value; + match field_value { + // NOTE: It is very important to still produce outputs for None field values. This is what + // enables field autocomplete in Rust Analyzer + Some(field_value_type!()) | None => { + if field.is_template { + assignments + .push(quote! {#(#field_path.)*#field_name = #bevy_ecs::template::TemplateField::Template(#field_value);}); + } else { + assignments + .push(quote! {#(#field_path.)*#field_name = #field_value;}); + } + } + Some(BsnValue::Type(field_type)) => { + if field_type.enum_variant.is_some() { + assignments + .push(quote! {#(#field_path.)*#field_name = #field_type;}); + } else { + let mut new_field_path = field_path.to_vec(); + new_field_path.push(Member::Named(field_name.clone())); + field_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + assignments, + false, + &new_field_path, + false, + ); + } + } + } + } + } + BsnFields::Tuple(fields) => { + for (index, field) in fields.iter().enumerate() { + let field_index = Index::from(index); + let field_value = &field.value; + match field_value { + field_value_type!() => { + if field.is_template { + assignments.push( + quote! {#(#field_path.)*#field_index = #bevy_ecs::template::TemplateField::Template(#field_value);}, + ); + } else { + assignments.push( + quote! {#(#field_path.)*#field_index = #field_value;}, + ); + } + } + BsnValue::Type(field_type) => { + if field_type.enum_variant.is_some() { + assignments.push( + quote! {#(#field_path.)*#field_index = #field_type;}, + ); + } else { + let mut new_field_path = field_path.to_vec(); + new_field_path.push(Member::Unnamed(field_index)); + field_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + assignments, + false, + &new_field_path, + false, + ); + } + } + } + } + } + } + } + } +} + +impl BsnSceneListItems { + pub fn to_tokens(&self, bevy_scene: &Path, bevy_ecs: &Path, bevy_asset: &Path) -> TokenStream { + let scenes = self + .0 + .iter() + .map(|scene| match scene { + BsnSceneListItem::Scene(bsn) => { + let tokens = bsn.to_tokens(bevy_scene, bevy_ecs, bevy_asset); + quote!{#bevy_scene::EntityScene(#tokens)} + }, + BsnSceneListItem::Expression(block) => quote!{#block}, + }); + quote! { + (#(#scenes,)*) + } + } +} + +impl ToTokens for BsnType { + fn to_tokens(&self, tokens: &mut TokenStream) { + let path = &self.path; + let maybe_variant = if let Some(variant) = &self.enum_variant { + Some(quote!{::#variant}) + } else { + None + }; + let result = match &self.fields { + BsnFields::Named(fields) => { + let assignments =fields.iter().map(|f| { + let name= &f.name; + let value = &f.value; + quote!{#name: #value} + }); + quote!{ + #path #maybe_variant { + #(#assignments,)* + } + } + }, + BsnFields::Tuple(fields) => { + let assignments =fields.iter().map(|f| { + &f.value + }); + quote!{ + #path #maybe_variant ( + #(#assignments,)* + ) + } + }, + }; + result.to_tokens(tokens); + } +} + + +impl ToTokens for BsnValue { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + BsnValue::Expr(expr_tokens) => { + quote!{{#expr_tokens}.into()}.to_tokens(tokens); + } + BsnValue::Closure(closure_tokens) => { + quote!{(#closure_tokens).into()}.to_tokens(tokens); + } + BsnValue::Ident(ident) => { + quote!{(#ident).into()}.to_tokens(tokens); + } + BsnValue::Lit(lit) => match lit { + Lit::Str(str) => quote! {#str.into()}.to_tokens(tokens), + _ => lit.to_tokens(tokens), + }, + BsnValue::Tuple(tuple) => { + let tuple_tokens = tuple.0.iter(); + quote! {(#(#tuple_tokens),*)}.to_tokens(tokens); + }, + BsnValue::Type(ty) => { + ty.to_tokens(tokens); + } + }; + } +} diff --git a/crates/bevy_scene2/macros/src/bsn/mod.rs b/crates/bevy_scene2/macros/src/bsn/mod.rs new file mode 100644 index 0000000000000..3e308604a64a0 --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/mod.rs @@ -0,0 +1,26 @@ +use crate::bsn::types::{BsnRoot, BsnSceneListItems}; +use bevy_macro_utils::BevyManifest; +use proc_macro::TokenStream; +use syn::parse_macro_input; + +pub mod codegen; +pub mod parse; +pub mod types; + +pub fn bsn(input: TokenStream) -> TokenStream { + let scene = parse_macro_input!(input as BsnRoot); + let manifest = BevyManifest::shared(); + let bevy_scene = manifest.get_path("bevy_scene2"); + let bevy_ecs = manifest.get_path("bevy_ecs"); + let bevy_asset = manifest.get_path("bevy_asset"); + TokenStream::from(scene.to_tokens(&bevy_scene, &bevy_ecs, &bevy_asset)) +} + +pub fn bsn_list(input: TokenStream) -> TokenStream { + let scene = parse_macro_input!(input as BsnSceneListItems); + let manifest = BevyManifest::shared(); + let bevy_scene = manifest.get_path("bevy_scene2"); + let bevy_ecs = manifest.get_path("bevy_ecs"); + let bevy_asset = manifest.get_path("bevy_asset"); + TokenStream::from(scene.to_tokens(&bevy_scene, &bevy_ecs, &bevy_asset)) +} diff --git a/crates/bevy_scene2/macros/src/bsn/parse.rs b/crates/bevy_scene2/macros/src/bsn/parse.rs new file mode 100644 index 0000000000000..0306f763642e0 --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/parse.rs @@ -0,0 +1,453 @@ +use crate::bsn::types::{ + Bsn, BsnConstructor, BsnEntry, BsnFields, BsnInheritedScene, BsnNamedField, + BsnRelatedSceneList, BsnRoot, BsnSceneList, BsnSceneListItem, BsnSceneListItems, BsnTuple, + BsnType, BsnUnnamedField, BsnValue, +}; +use proc_macro2::{Delimiter, TokenStream, TokenTree}; +use quote::quote; +use syn::{ + braced, bracketed, + buffer::Cursor, + parenthesized, + parse::{Parse, ParseBuffer, ParseStream}, + spanned::Spanned, + token::{At, Brace, Bracket, Colon, Comma, Paren}, + Block, Expr, Ident, Lit, LitStr, Path, Result, Token, +}; + +/// Functionally identical to [`Punctuated`](syn::punctuated::Punctuated), but fills the given `$list` Vec instead +/// of allocating a new one inside [`Punctuated`](syn::punctuated::Punctuated). This exists to avoid allocating an intermediate Vec. +macro_rules! parse_punctuated_vec { + ($list:ident, $input:ident, $parse:ident, $separator:ident) => { + loop { + if $input.is_empty() { + break; + } + let value = $input.parse::<$parse>()?; + $list.push(value); + if $input.is_empty() { + break; + } + $input.parse::<$separator>()?; + } + }; +} + +impl Parse for BsnRoot { + fn parse(input: ParseStream) -> Result { + Ok(BsnRoot(input.parse::>()?)) + } +} + +impl Parse for Bsn { + fn parse(input: ParseStream) -> Result { + let mut entries = Vec::new(); + if input.peek(Paren) { + let content; + parenthesized![content in input]; + while !content.is_empty() { + entries.push(content.parse::()?); + } + } else { + if ALLOW_FLAT { + while !input.is_empty() { + entries.push(input.parse::()?); + if input.peek(Comma) { + // Not ideal, but this anticipatory break allows us to parse non-parenthesized + // flat Bsn entries in SceneLists + break; + } + } + } else { + entries.push(input.parse::()?); + } + } + + Ok(Self { entries }) + } +} + +impl Parse for BsnEntry { + fn parse(input: ParseStream) -> Result { + Ok(if input.peek(Token![:]) { + BsnEntry::InheritedScene(input.parse::()?) + } else if input.peek(Token![#]) { + input.parse::()?; + BsnEntry::Name(input.parse::()?) + } else if input.peek(Brace) { + BsnEntry::SceneExpression(braced_tokens(input)?) + } else if input.peek(Bracket) { + BsnEntry::ChildrenSceneList(input.parse::()?) + } else { + let is_template = input.peek(At); + if is_template { + input.parse::()?; + } + let mut path = input.parse::()?; + let path_type = PathType::new(&path); + match path_type { + PathType::Type | PathType::Enum => { + let enum_variant = if matches!(path_type, PathType::Enum) { + take_last_path_ident(&mut path) + } else { + None + }; + if input.peek(Bracket) { + // TODO: fail if this is an enum variant + BsnEntry::RelatedSceneList(BsnRelatedSceneList { + relationship_path: path, + scene_list: input.parse::()?, + }) + } else { + let fields = input.parse::()?; + let bsn_type = BsnType { + path, + enum_variant, + fields, + }; + if is_template { + BsnEntry::TemplatePatch(bsn_type) + } else { + BsnEntry::GetTemplatePatch(bsn_type) + } + } + } + PathType::TypeConst => { + let const_ident = take_last_path_ident(&mut path).unwrap(); + BsnEntry::TemplateConst { + type_path: path, + const_ident, + } + } + PathType::Const => { + todo!("A floating type-unknown const should be assumed to be a const scene right?") + } + PathType::TypeFunction => { + let function = take_last_path_ident(&mut path).unwrap(); + let args = if input.peek(Paren) { + let content; + parenthesized!(content in input); + Some(content.parse_terminated(Expr::parse, Token![,])?) + } else { + None + }; + + let bsn_constructor = BsnConstructor { + type_path: path, + function, + args, + }; + if is_template { + BsnEntry::TemplateConstructor(bsn_constructor) + } else { + BsnEntry::GetTemplateConstructor(bsn_constructor) + } + } + PathType::Function => { + if input.peek(Paren) { + let tokens = parenthesized_tokens(input)?; + BsnEntry::SceneExpression(quote! {#path(#tokens)}) + } else { + BsnEntry::SceneExpression(quote! {#path}) + } + } + } + }) + } +} + +impl Parse for BsnSceneList { + fn parse(input: ParseStream) -> Result { + let content; + bracketed!(content in input); + Ok(BsnSceneList(content.parse::()?)) + } +} + +impl Parse for BsnSceneListItems { + fn parse(input: ParseStream) -> Result { + let mut scenes = Vec::new(); + parse_punctuated_vec!(scenes, input, BsnSceneListItem, Comma); + Ok(BsnSceneListItems(scenes)) + } +} + +impl Parse for BsnSceneListItem { + fn parse(input: ParseStream) -> Result { + Ok(if input.peek(Brace) { + BsnSceneListItem::Expression(input.parse::()?) + } else { + BsnSceneListItem::Scene(input.parse::>()?) + }) + } +} + +impl Parse for BsnInheritedScene { + fn parse(input: ParseStream) -> Result { + input.parse::()?; + Ok(if input.peek(LitStr) { + let path = input.parse::()?; + BsnInheritedScene::Asset(path) + } else { + let function = input.parse::()?; + let args = if input.peek(Paren) { + let content; + parenthesized!(content in input); + Some(content.parse_terminated(Expr::parse, Token![,])?) + } else { + None + }; + BsnInheritedScene::Fn { function, args } + }) + } +} + +impl Parse for BsnType { + fn parse(input: ParseStream) -> Result { + let mut path = input.parse::()?; + let enum_variant = match PathType::new(&path) { + PathType::Type => None, + PathType::Enum => take_last_path_ident(&mut path), + PathType::Function | PathType::TypeFunction => { + return Err(syn::Error::new( + path.span(), + "Expected a path to a BSN type but encountered a path to a function.", + )) + } + PathType::Const | PathType::TypeConst => { + return Err(syn::Error::new( + path.span(), + "Expected a path to a BSN type but encountered a path to a const.", + )) + } + }; + let fields = input.parse::()?; + Ok(BsnType { + path, + enum_variant, + fields, + }) + } +} + +impl Parse for BsnTuple { + fn parse(input: ParseStream) -> Result { + let content; + parenthesized![content in input]; + let mut fields = Vec::new(); + while !content.is_empty() { + fields.push(content.parse::()?); + } + Ok(BsnTuple(fields)) + } +} + +impl Parse for BsnFields { + fn parse(input: ParseStream) -> Result { + Ok(if input.peek(Brace) { + let content; + braced![content in input]; + let mut fields = Vec::new(); + parse_punctuated_vec!(fields, content, BsnNamedField, Comma); + BsnFields::Named(fields) + } else if input.peek(Paren) { + let content; + parenthesized![content in input]; + let mut fields = Vec::new(); + parse_punctuated_vec!(fields, content, BsnUnnamedField, Comma); + BsnFields::Tuple(fields) + } else { + BsnFields::Named(Vec::new()) + }) + } +} + +impl Parse for BsnNamedField { + fn parse(input: ParseStream) -> Result { + let name = input.parse::()?; + let mut is_template = false; + let value = if input.peek(Colon) { + input.parse::()?; + if input.peek(At) { + input.parse::()?; + is_template = true; + } + Some(input.parse::()?) + } else { + None + }; + Ok(BsnNamedField { + name, + value, + is_template, + }) + } +} + +impl Parse for BsnUnnamedField { + fn parse(input: ParseStream) -> Result { + let mut is_template = false; + if input.peek(At) { + input.parse::()?; + is_template = true; + } + let value = input.parse::()?; + Ok(BsnUnnamedField { value, is_template }) + } +} + +/// Parse a closure "loosely" without caring about the tokens between `|...|` and `{...}`. This ensures autocomplete works. +fn parse_closure_loose<'a>(input: &'a ParseBuffer) -> Result { + let start = input.cursor(); + input.parse::()?; + let tokens = input.step(|cursor| { + let mut rest = *cursor; + while let Some((tt, next)) = rest.token_tree() { + match &tt { + TokenTree::Punct(punct) if punct.as_char() == '|' => { + if let Some((TokenTree::Group(group), next)) = next.token_tree() + && group.delimiter() == Delimiter::Brace + { + return Ok((tokens_between(start, next), next)); + } else { + return Err(cursor.error("closures expect '{' to follow '|'")); + } + } + _ => rest = next, + } + } + Err(cursor.error("no matching `|` was found after this point")) + })?; + Ok(tokens) +} + +// Used to parse a block "loosely" without caring about the content in `{...}`. This ensures autocomplete works. +fn braced_tokens<'a>(input: &'a ParseBuffer) -> Result { + let content; + braced!(content in input); + Ok(content.parse::()?) +} + +// Used to parse parenthesized tokens "loosely" without caring about the content in `(...)`. This ensures autocomplete works. +fn parenthesized_tokens<'a>(input: &'a ParseBuffer) -> Result { + let content; + parenthesized!(content in input); + Ok(content.parse::()?) +} + +fn tokens_between(begin: Cursor, end: Cursor) -> TokenStream { + assert!(begin <= end); + let mut cursor = begin; + let mut tokens = TokenStream::new(); + while cursor < end { + let (token, next) = cursor.token_tree().unwrap(); + tokens.extend(std::iter::once(token)); + cursor = next; + } + tokens +} + +impl Parse for BsnValue { + fn parse(input: ParseStream) -> Result { + Ok(if input.peek(Brace) { + BsnValue::Expr(braced_tokens(input)?) + } else if input.peek(Token![|]) { + let tokens = parse_closure_loose(input)?; + BsnValue::Closure(tokens) + } else if input.peek(Ident) { + let forked = input.fork(); + let path = forked.parse::()?; + if path.segments.len() == 1 && (forked.is_empty() || forked.peek(Comma)) { + return Ok(BsnValue::Ident(input.parse::()?)); + } + match PathType::new(&path) { + PathType::TypeFunction | PathType::Function => { + input.parse::()?; + let token_stream = parenthesized_tokens(input)?; + BsnValue::Expr(quote! { #path(#token_stream) }) + } + PathType::Const | PathType::TypeConst => { + input.parse::()?; + BsnValue::Expr(quote! { #path }) + } + PathType::Type | PathType::Enum => BsnValue::Type(input.parse::()?), + } + } else if input.peek(Lit) { + BsnValue::Lit(input.parse::()?) + } else if input.peek(Paren) { + BsnValue::Tuple(input.parse::()?) + } else { + return Err(input.error( + "BsnValue parse for this input is not supported yet, nor is proper error handling :)" + )); + }) + } +} + +enum PathType { + Type, + Enum, + Const, + TypeConst, + TypeFunction, + Function, +} + +impl PathType { + fn new(path: &Path) -> PathType { + let mut iter = path.segments.iter().rev(); + if let Some(last_segment) = iter.next() { + let last_string = last_segment.ident.to_string(); + let mut last_string_chars = last_string.chars(); + let last_ident_first_char = last_string_chars.next().unwrap(); + let is_const = last_string_chars + .next() + .map(|last_ident_second_char| last_ident_second_char.is_uppercase()) + .unwrap_or(false); + if last_ident_first_char.is_uppercase() { + if let Some(second_to_last_segment) = iter.next() { + // PERF: is there some way to avoid this string allocation? + let second_to_last_string = second_to_last_segment.ident.to_string(); + let first_char = second_to_last_string.chars().next().unwrap(); + if first_char.is_uppercase() { + if is_const { + PathType::TypeConst + } else { + PathType::Enum + } + } else { + if is_const { + PathType::Const + } else { + PathType::Type + } + } + } else { + PathType::Type + } + } else { + if let Some(second_to_last) = iter.next() { + // PERF: is there some way to avoid this string allocation? + let second_to_last_string = second_to_last.ident.to_string(); + let first_char = second_to_last_string.chars().next().unwrap(); + if first_char.is_uppercase() { + PathType::TypeFunction + } else { + PathType::Function + } + } else { + PathType::Function + } + } + } else { + // This won't be hit so just pick one to make it easy on consumers + PathType::Type + } + } +} + +fn take_last_path_ident(path: &mut Path) -> Option { + let ident = path.segments.pop().map(|s| s.into_value().ident); + path.segments.pop_punct(); + ident +} diff --git a/crates/bevy_scene2/macros/src/bsn/types.rs b/crates/bevy_scene2/macros/src/bsn/types.rs new file mode 100644 index 0000000000000..81c1484a7a553 --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/types.rs @@ -0,0 +1,99 @@ +use proc_macro2::TokenStream; +use syn::{punctuated::Punctuated, Block, Expr, Ident, Lit, LitStr, Path, Token}; + +#[derive(Debug)] +pub struct BsnRoot(pub Bsn); + +#[derive(Debug)] +pub struct Bsn { + pub entries: Vec, +} + +#[derive(Debug)] +pub enum BsnEntry { + Name(Ident), + GetTemplatePatch(BsnType), + TemplatePatch(BsnType), + GetTemplateConstructor(BsnConstructor), + TemplateConstructor(BsnConstructor), + TemplateConst { type_path: Path, const_ident: Ident }, + SceneExpression(TokenStream), + InheritedScene(BsnInheritedScene), + RelatedSceneList(BsnRelatedSceneList), + ChildrenSceneList(BsnSceneList), +} + +#[derive(Debug)] +pub struct BsnType { + pub path: Path, + pub enum_variant: Option, + pub fields: BsnFields, +} + +#[derive(Debug)] +pub struct BsnRelatedSceneList { + pub relationship_path: Path, + pub scene_list: BsnSceneList, +} + +#[derive(Debug)] +pub struct BsnSceneList(pub BsnSceneListItems); + +#[derive(Debug)] +pub struct BsnSceneListItems(pub Vec); + +#[derive(Debug)] +pub enum BsnSceneListItem { + Scene(Bsn), + Expression(Block), +} + +#[derive(Debug)] +pub enum BsnInheritedScene { + Asset(LitStr), + Fn { + function: Ident, + args: Option>, + }, +} + +#[derive(Debug)] +pub struct BsnConstructor { + pub type_path: Path, + pub function: Ident, + pub args: Option>, +} + +#[derive(Debug)] +pub enum BsnFields { + Named(Vec), + Tuple(Vec), +} + +#[derive(Debug)] +pub struct BsnTuple(pub Vec); + +#[derive(Debug)] +pub struct BsnNamedField { + pub name: Ident, + /// This is an Option to enable autocomplete when the field name is being typed + /// To improve autocomplete further we'll need to forgo a lot of the syn parsing + pub value: Option, + pub is_template: bool, +} + +#[derive(Debug)] +pub struct BsnUnnamedField { + pub value: BsnValue, + pub is_template: bool, +} + +#[derive(Debug)] +pub enum BsnValue { + Expr(TokenStream), + Closure(TokenStream), + Ident(Ident), + Lit(Lit), + Type(BsnType), + Tuple(BsnTuple), +} diff --git a/crates/bevy_scene2/macros/src/lib.rs b/crates/bevy_scene2/macros/src/lib.rs new file mode 100644 index 0000000000000..f93d495211687 --- /dev/null +++ b/crates/bevy_scene2/macros/src/lib.rs @@ -0,0 +1,13 @@ +mod bsn; + +use proc_macro::TokenStream; + +#[proc_macro] +pub fn bsn(input: TokenStream) -> TokenStream { + crate::bsn::bsn(input) +} + +#[proc_macro] +pub fn bsn_list(input: TokenStream) -> TokenStream { + crate::bsn::bsn_list(input) +} diff --git a/crates/bevy_scene2/src/lib.rs b/crates/bevy_scene2/src/lib.rs new file mode 100644 index 0000000000000..cb8ed3e0c7de2 --- /dev/null +++ b/crates/bevy_scene2/src/lib.rs @@ -0,0 +1,97 @@ +#![allow(missing_docs)] + +pub mod prelude { + pub use crate::{ + bsn, bsn_list, on, CommandsSpawnScene, LoadScene, PatchGetTemplate, PatchTemplate, Scene, + SceneList, ScenePatchInstance, SpawnScene, + }; +} + +mod resolved_scene; +mod scene; +mod scene_list; +mod scene_patch; +mod spawn; + +pub use bevy_scene2_macros::*; + +pub use resolved_scene::*; +pub use scene::*; +pub use scene_list::*; +pub use scene_patch::*; +pub use spawn::*; + +use bevy_app::{App, Plugin, Update}; +use bevy_asset::{AssetApp, AssetPath, AssetServer, Handle}; +use bevy_ecs::{prelude::*, system::IntoObserverSystem, template::Template}; +use std::marker::PhantomData; + +#[derive(Default)] +pub struct ScenePlugin; + +impl Plugin for ScenePlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_asset::() + .add_systems(Update, (resolve_scene_patches, spawn_queued).chain()); + } +} + +/// This is used by the [`bsn!`] macro to generate compile-time only references to symbols. Currently this is used +/// to add IDE support for nested type names, as it allows us to pass the input Ident from the input to the output code. +pub const fn touch_type() {} + +pub trait LoadScene { + fn load_scene<'a>( + &self, + path: impl Into>, + scene: impl Scene, + ) -> Handle; +} + +impl LoadScene for AssetServer { + fn load_scene<'a>( + &self, + path: impl Into>, + scene: impl Scene, + ) -> Handle { + let scene = ScenePatch::load(self, scene); + self.load_with_path(path, scene) + } +} + +pub struct OnTemplate(pub I, pub PhantomData (E, B, M)>); + +impl + Clone, E: EntityEvent, B: Bundle, M: 'static> Template + for OnTemplate +{ + type Output = (); + + fn build(&mut self, entity: &mut EntityWorldMut) -> Result { + entity.observe(self.0.clone()); + Ok(()) + } +} + +impl< + I: IntoObserverSystem + Clone + Send + Sync, + E: EntityEvent, + B: Bundle, + M: 'static, + > Scene for OnTemplate +{ + fn patch( + &self, + _assets: &AssetServer, + _patches: &bevy_asset::Assets, + scene: &mut ResolvedScene, + ) { + scene.push_template(OnTemplate(self.0.clone(), PhantomData)); + } +} + +pub fn on, E: EntityEvent, B: Bundle, M: 'static>( + observer: I, +) -> OnTemplate { + OnTemplate(observer, PhantomData) +} diff --git a/crates/bevy_scene2/src/resolved_scene.rs b/crates/bevy_scene2/src/resolved_scene.rs new file mode 100644 index 0000000000000..01f68b03265fb --- /dev/null +++ b/crates/bevy_scene2/src/resolved_scene.rs @@ -0,0 +1,78 @@ +use bevy_ecs::{ + bundle::Bundle, + entity::Entity, + error::Result, + relationship::Relationship, + template::{ErasedTemplate, Template}, + world::EntityWorldMut, +}; +use bevy_utils::TypeIdMap; +use std::any::TypeId; + +#[derive(Default)] +pub struct ResolvedScene { + pub template_indices: TypeIdMap, + pub templates: Vec>, + // PERF: special casing children probably makes sense here + pub related: TypeIdMap, +} + +impl ResolvedScene { + pub fn spawn(&mut self, entity: &mut EntityWorldMut) -> Result { + for template in self.templates.iter_mut() { + template.apply(entity)?; + } + + for related in self.related.values_mut() { + let target = entity.id(); + entity.world_scope(|world| -> Result { + for scene in &mut related.scenes { + let mut entity = world.spawn_empty(); + (related.insert)(&mut entity, target); + // PERF: this will result in an archetype move + scene.spawn(&mut entity)?; + } + Ok(()) + })?; + } + + Ok(()) + } + + pub fn get_or_insert_template + Default + Send + Sync + 'static>( + &mut self, + ) -> &mut T { + let index = self + .template_indices + .entry(TypeId::of::()) + .or_insert_with(|| { + let index = self.templates.len(); + self.templates.push(Box::new(T::default())); + index + }); + self.templates[*index].downcast_mut::().unwrap() + } + + pub fn push_template + Send + Sync + 'static>( + &mut self, + template: T, + ) { + self.templates.push(Box::new(template)); + } +} + +pub struct ResolvedRelatedScenes { + pub scenes: Vec, + pub insert: fn(&mut EntityWorldMut, target: Entity), +} + +impl ResolvedRelatedScenes { + pub fn new() -> Self { + Self { + scenes: Vec::new(), + insert: |entity, target| { + entity.insert(R::from(target)); + }, + } + } +} diff --git a/crates/bevy_scene2/src/scene.rs b/crates/bevy_scene2/src/scene.rs new file mode 100644 index 0000000000000..75c77d7f0f8b4 --- /dev/null +++ b/crates/bevy_scene2/src/scene.rs @@ -0,0 +1,169 @@ +use crate::{ResolvedRelatedScenes, ResolvedScene, SceneList, ScenePatch}; +use bevy_asset::{AssetPath, AssetServer, Assets}; +use bevy_ecs::{ + bundle::Bundle, + error::Result, + relationship::Relationship, + template::{FnTemplate, GetTemplate, Template}, + world::EntityWorldMut, +}; +use std::{any::TypeId, marker::PhantomData}; +use variadics_please::all_tuples; + +pub trait Scene: Send + Sync + 'static { + fn patch(&self, assets: &AssetServer, patches: &Assets, scene: &mut ResolvedScene); + fn register_dependencies(&self, _dependencies: &mut Vec>) {} +} + +macro_rules! scene_impl { + ($($patch: ident),*) => { + impl<$($patch: Scene),*> Scene for ($($patch,)*) { + fn patch(&self, _assets: &AssetServer, _patches: &Assets, _scene: &mut ResolvedScene) { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($patch,)*) = self; + $($patch.patch(_assets, _patches, _scene);)* + } + + fn register_dependencies(&self, _dependencies: &mut Vec>) { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($patch,)*) = self; + $($patch.register_dependencies(_dependencies);)* + } + } + } +} + +all_tuples!(scene_impl, 0, 12, P); + +pub struct TemplatePatch(pub F, pub PhantomData); + +pub fn template_value( + value: T, +) -> TemplatePatch { + TemplatePatch( + move |input: &mut T| { + *input = value.clone(); + }, + PhantomData, + ) +} + +pub trait PatchGetTemplate { + type Template; + fn patch(func: F) -> TemplatePatch; +} + +impl PatchGetTemplate for G { + type Template = G::Template; + fn patch(func: F) -> TemplatePatch { + TemplatePatch(func, PhantomData) + } +} + +pub trait PatchTemplate: Sized { + fn patch_template(func: F) -> TemplatePatch; +} + +impl PatchTemplate for T { + fn patch_template(func: F) -> TemplatePatch { + TemplatePatch(func, PhantomData) + } +} + +impl< + F: Fn(&mut T) + Send + Sync + 'static, + T: Template + Send + Sync + Default + 'static, + > Scene for TemplatePatch +{ + fn patch( + &self, + _assets: &AssetServer, + _patches: &Assets, + scene: &mut ResolvedScene, + ) { + let template = scene.get_or_insert_template::(); + (self.0)(template); + } +} + +pub struct RelatedScenes { + pub related_template_list: L, + pub marker: PhantomData, +} + +impl RelatedScenes { + pub fn new(list: L) -> Self { + Self { + related_template_list: list, + marker: PhantomData, + } + } +} + +impl Scene for RelatedScenes { + fn patch(&self, assets: &AssetServer, patches: &Assets, scene: &mut ResolvedScene) { + let related = scene + .related + .entry(TypeId::of::()) + .or_insert_with(ResolvedRelatedScenes::new::); + self.related_template_list + .patch_list(assets, patches, &mut related.scenes); + } + + fn register_dependencies(&self, dependencies: &mut Vec>) { + self.related_template_list + .register_dependencies(dependencies); + } +} + +pub struct InheritScene(pub S); + +impl Scene for InheritScene { + fn patch(&self, assets: &AssetServer, patches: &Assets, scene: &mut ResolvedScene) { + self.0.patch(assets, patches, scene); + } + + fn register_dependencies(&self, dependencies: &mut Vec>) { + self.0.register_dependencies(dependencies); + } +} + +#[derive(Clone)] +pub struct InheritSceneAsset(pub AssetPath<'static>); + +impl>> From for InheritSceneAsset { + fn from(value: I) -> Self { + InheritSceneAsset(value.into()) + } +} + +impl Scene for InheritSceneAsset { + fn patch(&self, assets: &AssetServer, patches: &Assets, scene: &mut ResolvedScene) { + let id = assets.get_path_id(&self.0).unwrap(); + let scene_patch = patches.get(id.typed()).unwrap(); + scene_patch.patch.patch(assets, patches, scene); + } + + fn register_dependencies(&self, dependencies: &mut Vec>) { + dependencies.push(self.0.clone()) + } +} + +impl Result) + Clone + Send + Sync + 'static, O: Bundle> Scene + for FnTemplate +{ + fn patch( + &self, + _assets: &AssetServer, + _patches: &Assets, + scene: &mut ResolvedScene, + ) { + scene.push_template(FnTemplate(self.0.clone())); + } +} diff --git a/crates/bevy_scene2/src/scene_list.rs b/crates/bevy_scene2/src/scene_list.rs new file mode 100644 index 0000000000000..8047767f52fe1 --- /dev/null +++ b/crates/bevy_scene2/src/scene_list.rs @@ -0,0 +1,101 @@ +use crate::{ResolvedScene, Scene, ScenePatch}; +use bevy_asset::{AssetPath, AssetServer, Assets}; +use variadics_please::all_tuples; + +pub trait SceneList: Send + Sync + 'static { + fn patch_list( + &self, + assets: &AssetServer, + patches: &Assets, + scenes: &mut Vec, + ); + + fn register_dependencies(&self, dependencies: &mut Vec>); +} + +pub struct EntityScene(pub S); + +impl SceneList for EntityScene { + fn patch_list( + &self, + assets: &AssetServer, + patches: &Assets, + scenes: &mut Vec, + ) { + let mut resolved_scene = ResolvedScene::default(); + self.0.patch(assets, patches, &mut resolved_scene); + scenes.push(resolved_scene); + } + + fn register_dependencies(&self, dependencies: &mut Vec>) { + self.0.register_dependencies(dependencies); + } +} + +macro_rules! scene_list_impl { + ($($list: ident),*) => { + impl<$($list: SceneList),*> SceneList for ($($list,)*) { + fn patch_list(&self, _assets: &AssetServer, _patches: &Assets, _scenes: &mut Vec) { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($list,)*) = self; + $($list.patch_list(_assets, _patches, _scenes);)* + } + + fn register_dependencies(&self, _dependencies: &mut Vec>) { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($list,)*) = self; + $($list.register_dependencies(_dependencies);)* + } + } + } +} + +all_tuples!(scene_list_impl, 0, 12, P); + +impl SceneList for Vec { + fn patch_list( + &self, + assets: &AssetServer, + patches: &Assets, + scenes: &mut Vec, + ) { + for scene in self { + let mut resolved_scene = ResolvedScene::default(); + scene.patch(assets, patches, &mut resolved_scene); + scenes.push(resolved_scene); + } + } + + fn register_dependencies(&self, dependencies: &mut Vec>) { + for scene in self { + scene.register_dependencies(dependencies); + } + } +} + +impl SceneList for Vec> { + fn patch_list( + &self, + assets: &AssetServer, + patches: &Assets, + scenes: &mut Vec, + ) { + for scene in self { + let mut resolved_scene = ResolvedScene::default(); + scene.patch(assets, patches, &mut resolved_scene); + scenes.push(resolved_scene); + } + } + + fn register_dependencies(&self, dependencies: &mut Vec>) { + for scene in self { + scene.register_dependencies(dependencies); + } + } +} diff --git a/crates/bevy_scene2/src/scene_patch.rs b/crates/bevy_scene2/src/scene_patch.rs new file mode 100644 index 0000000000000..cc5c090feaeac --- /dev/null +++ b/crates/bevy_scene2/src/scene_patch.rs @@ -0,0 +1,33 @@ +use crate::{ResolvedScene, Scene}; +use bevy_asset::{Asset, AssetServer, Handle, UntypedHandle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Component; +use bevy_reflect::TypePath; + +#[derive(Asset, TypePath)] +pub struct ScenePatch { + pub patch: Box, + #[dependency] + pub dependencies: Vec, + // TODO: consider breaking this out to prevent mutating asset events when resolved + pub resolved: Option, +} + +impl ScenePatch { + pub fn load(assets: &AssetServer, scene: P) -> Self { + let mut dependencies = Vec::new(); + scene.register_dependencies(&mut dependencies); + let dependencies = dependencies + .iter() + .map(|i| assets.load::(i.clone()).untyped()) + .collect::>(); + ScenePatch { + patch: Box::new(scene), + dependencies, + resolved: None, + } + } +} + +#[derive(Component, Deref, DerefMut)] +pub struct ScenePatchInstance(pub Handle); diff --git a/crates/bevy_scene2/src/spawn.rs b/crates/bevy_scene2/src/spawn.rs new file mode 100644 index 0000000000000..e40c8e773c7fe --- /dev/null +++ b/crates/bevy_scene2/src/spawn.rs @@ -0,0 +1,107 @@ +use crate::{ResolvedScene, Scene, ScenePatch, ScenePatchInstance}; +use bevy_asset::{AssetEvent, AssetId, AssetServer, Assets}; +use bevy_ecs::{event::EventCursor, prelude::*}; +use bevy_platform::collections::HashMap; + +pub trait SpawnScene { + fn spawn_scene(&mut self, scene: S) -> EntityWorldMut; +} + +impl SpawnScene for World { + fn spawn_scene(&mut self, scene: S) -> EntityWorldMut { + let assets = self.resource::(); + let patch = ScenePatch::load(assets, scene); + let handle = assets.add(patch); + self.spawn(ScenePatchInstance(handle)) + } +} + +pub trait CommandsSpawnScene { + fn spawn_scene(&mut self, scene: S) -> EntityCommands; +} + +impl<'w, 's> CommandsSpawnScene for Commands<'w, 's> { + fn spawn_scene(&mut self, scene: S) -> EntityCommands { + let mut entity_commands = self.spawn_empty(); + let id = entity_commands.id(); + entity_commands.commands().queue(move |world: &mut World| { + let assets = world.resource::(); + let patch = ScenePatch::load(assets, scene); + let handle = assets.add(patch); + if let Ok(mut entity) = world.get_entity_mut(id) { + entity.insert(ScenePatchInstance(handle)); + } + }); + entity_commands + } +} + +pub fn resolve_scene_patches( + mut events: EventReader>, + assets: Res, + mut patches: ResMut>, +) { + for event in events.read() { + match *event { + // TODO: handle modified? + AssetEvent::LoadedWithDependencies { id } => { + let mut scene = ResolvedScene::default(); + // TODO: real error handling + let patch = patches.get(id).unwrap(); + patch.patch.patch(&assets, &patches, &mut scene); + let patch = patches.get_mut(id).unwrap(); + patch.resolved = Some(scene) + } + _ => {} + } + } +} + +#[derive(Resource, Default)] +pub struct QueuedScenes { + waiting_entities: HashMap, Vec>, +} + +pub fn spawn_queued( + world: &mut World, + handles: &mut QueryState<(Entity, &ScenePatchInstance), Added>, + mut reader: Local>>, +) { + world.resource_scope(|world, mut patches: Mut>| { + world.resource_scope(|world, mut queued: Mut| { + world.resource_scope(|world, events: Mut>>| { + for (entity, id) in handles + .iter(world) + .map(|(e, h)| (e, h.id())) + .collect::>() + { + if let Some(scene) = patches.get_mut(id).and_then(|p| p.resolved.as_mut()) { + let mut entity_mut = world.get_entity_mut(entity).unwrap(); + scene.spawn(&mut entity_mut).unwrap(); + } else { + let entities = queued.waiting_entities.entry(id).or_default(); + entities.push(entity); + } + } + + for event in reader.read(&events) { + if let AssetEvent::LoadedWithDependencies { id } = event { + let Some(scene) = patches.get_mut(*id).and_then(|p| p.resolved.as_mut()) + else { + continue; + }; + + let Some(entities) = queued.waiting_entities.remove(id) else { + continue; + }; + + for entity in entities { + let mut entity_mut = world.get_entity_mut(entity).unwrap(); + scene.spawn(&mut entity_mut).unwrap(); + } + } + } + }); + }); + }); +} diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index f71d8c63f7e80..8b478bb68bfa2 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -453,10 +453,16 @@ pub struct RenderWireframeMaterial { pub color: [f32; 4], } -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq)] #[reflect(Component, Default, Clone, PartialEq)] pub struct Mesh2dWireframe(pub Handle); +impl Default for Mesh2dWireframe { + fn default() -> Self { + Self(Handle::default()) + } +} + impl AsAssetId for Mesh2dWireframe { type Asset = Wireframe2dMaterial; diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 39e215df0406e..4d06700bc8457 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -14,7 +14,7 @@ use bevy_transform::components::Transform; use crate::TextureSlicer; /// Describes a sprite to be rendered to a 2D camera -#[derive(Component, Debug, Default, Clone, Reflect)] +#[derive(Component, Debug, Clone, Reflect)] #[require(Transform, Visibility, SyncToRenderWorld, VisibilityClass, Anchor)] #[reflect(Component, Default, Debug, Clone)] #[component(on_add = view::add_visibility_class::)] @@ -42,6 +42,21 @@ pub struct Sprite { pub image_mode: SpriteImageMode, } +impl Default for Sprite { + fn default() -> Self { + Self { + image: Handle::default(), + texture_atlas: Default::default(), + color: Default::default(), + flip_x: Default::default(), + flip_y: Default::default(), + custom_size: Default::default(), + rect: Default::default(), + image_mode: Default::default(), + } + } +} + impl Sprite { /// Create a Sprite with a custom size pub fn sized(custom_size: Vec2) -> Self { diff --git a/crates/bevy_sprite/src/tilemap_chunk/mod.rs b/crates/bevy_sprite/src/tilemap_chunk/mod.rs index 174816154bc6e..dc45d459338e4 100644 --- a/crates/bevy_sprite/src/tilemap_chunk/mod.rs +++ b/crates/bevy_sprite/src/tilemap_chunk/mod.rs @@ -45,7 +45,7 @@ pub struct TilemapChunkMeshCache(HashMap> /// A component representing a chunk of a tilemap. /// Each chunk is a rectangular section of tiles that is rendered as a single mesh. -#[derive(Component, Clone, Debug, Default)] +#[derive(Component, Clone, Debug)] #[require(Anchor)] #[component(immutable, on_insert = on_insert_tilemap_chunk)] pub struct TilemapChunk { @@ -60,6 +60,17 @@ pub struct TilemapChunk { pub alpha_mode: AlphaMode2d, } +impl Default for TilemapChunk { + fn default() -> Self { + Self { + chunk_size: Default::default(), + tile_display_size: Default::default(), + tileset: Handle::default(), + alpha_mode: Default::default(), + } + } +} + /// Component storing the indices of tiles within a chunk. /// Each index corresponds to a specific tile in the tileset. #[derive(Component, Clone, Debug, Deref, DerefMut)] diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 330f0d977a279..0a940f933864d 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -343,7 +343,7 @@ impl TextFont { impl Default for TextFont { fn default() -> Self { Self { - font: Default::default(), + font: Handle::default(), font_size: 20.0, line_height: LineHeight::default(), font_smoothing: Default::default(), diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs index b50f4cc245a64..9d6ee2d67ab9f 100644 --- a/crates/bevy_ui/src/interaction_states.rs +++ b/crates/bevy_ui/src/interaction_states.rs @@ -45,7 +45,7 @@ pub struct Pressed; pub struct Checkable; /// Component that indicates whether a checkbox or radio button is in a checked state. -#[derive(Component, Default, Debug)] +#[derive(Component, Default, Debug, Clone)] pub struct Checked; pub(crate) fn on_add_checkable(trigger: On, mut world: DeferredWorld) { diff --git a/crates/bevy_winit/src/custom_cursor.rs b/crates/bevy_winit/src/custom_cursor.rs index dd8236e30e43d..b441c6a14b623 100644 --- a/crates/bevy_winit/src/custom_cursor.rs +++ b/crates/bevy_winit/src/custom_cursor.rs @@ -8,7 +8,7 @@ use wgpu_types::TextureFormat; use crate::{cursor::CursorIcon, state::CustomCursorCache}; /// A custom cursor created from an image. -#[derive(Debug, Clone, Default, Reflect, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash)] #[reflect(Debug, Default, Hash, PartialEq, Clone)] pub struct CustomCursorImage { /// Handle to the image to use as the cursor. The image must be in 8 bit int @@ -42,6 +42,19 @@ pub struct CustomCursorImage { pub hotspot: (u16, u16), } +impl Default for CustomCursorImage { + fn default() -> Self { + Self { + handle: Handle::default(), + texture_atlas: Default::default(), + flip_x: Default::default(), + flip_y: Default::default(), + rect: Default::default(), + hotspot: Default::default(), + } + } +} + #[cfg(all(target_family = "wasm", target_os = "unknown"))] /// A custom cursor created from a URL. #[derive(Debug, Clone, Default, Reflect, PartialEq, Eq, Hash)] diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index 25106adcfb48f..4a657d14d4736 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -26,9 +26,15 @@ enum AppState { Finished, } -#[derive(Resource, Default)] +#[derive(Resource)] struct RpgSpriteFolder(Handle); +impl Default for RpgSpriteFolder { + fn default() -> Self { + Self(Handle::default()) + } +} + fn load_textures(mut commands: Commands, asset_server: Res) { // Load multiple, individual sprites from a folder commands.insert_resource(RpgSpriteFolder(asset_server.load_folder("textures/rpg"))); diff --git a/examples/asset/asset_decompression.rs b/examples/asset/asset_decompression.rs index e514924ca9d0a..354a01f907d31 100644 --- a/examples/asset/asset_decompression.rs +++ b/examples/asset/asset_decompression.rs @@ -87,12 +87,21 @@ impl AssetLoader for GzAssetLoader { } } -#[derive(Component, Default)] +#[derive(Component)] struct Compressed { compressed: Handle, _phantom: PhantomData, } +impl Default for Compressed { + fn default() -> Self { + Self { + compressed: Handle::default(), + _phantom: Default::default(), + } + } +} + fn main() { App::new() .add_plugins(DefaultPlugins) diff --git a/examples/asset/custom_asset.rs b/examples/asset/custom_asset.rs index 8d4ac958ecdd1..ee7ce4349a1e1 100644 --- a/examples/asset/custom_asset.rs +++ b/examples/asset/custom_asset.rs @@ -102,7 +102,7 @@ fn main() { .run(); } -#[derive(Resource, Default)] +#[derive(Resource)] struct State { handle: Handle, other_handle: Handle, @@ -110,6 +110,17 @@ struct State { printed: bool, } +impl Default for State { + fn default() -> Self { + Self { + handle: Handle::default(), + other_handle: Handle::default(), + blob: Handle::default(), + printed: Default::default(), + } + } +} + fn setup(mut state: ResMut, asset_server: Res) { // Recommended way to load an asset state.handle = asset_server.load("data/asset.custom"); diff --git a/examples/games/alien_cake_addict.rs b/examples/games/alien_cake_addict.rs index d0aa9c8680690..c2fabe0bfe8c5 100644 --- a/examples/games/alien_cake_addict.rs +++ b/examples/games/alien_cake_addict.rs @@ -58,7 +58,6 @@ struct Player { move_cooldown: Timer, } -#[derive(Default)] struct Bonus { entity: Option, i: usize, @@ -66,6 +65,17 @@ struct Bonus { handle: Handle, } +impl Default for Bonus { + fn default() -> Self { + Self { + entity: Default::default(), + i: Default::default(), + j: Default::default(), + handle: Handle::default(), + } + } +} + #[derive(Resource, Default)] struct Game { board: Vec>, diff --git a/examples/scene/bsn.rs b/examples/scene/bsn.rs new file mode 100644 index 0000000000000..22d73259fd94b --- /dev/null +++ b/examples/scene/bsn.rs @@ -0,0 +1,197 @@ +//! This is a temporary stress test of various bsn! features. +// TODO: move these into actual tests and replace this with a more instructive user-facing example +use bevy::{ + prelude::*, + scene2::prelude::{Scene, *}, +}; +use bevy_scene2::SceneList; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, (print::, print::, print::)) + .run(); +} + +fn setup(mut commands: Commands, assets: Res) { + assets.load_scene("scene://base.bsn", base()); + assets.load_scene("scene://transform_1000.bsn", transform_1000()); + let top_level_handle = assets.load_scene("scene://top_level.bsn", top_level()); + commands.spawn(ScenePatchInstance(top_level_handle)); +} + +fn top_level() -> impl Scene { + let a = 20usize; + let b = 1993usize; + bsn! { + #TopLevel + :"scene://base.bsn" + :x + Sprite { size: b } + Team::Green(10) + {transform_1337()} + Children [ + Sprite { size: {4 + a}} + ] + } +} + +fn base() -> impl Scene { + let sprites = (0..10usize) + .map(|i| bsn! {Sprite { size: {i} }}) + .collect::>(); + + bsn! { + Name("Base") + Sprite { + handle: "asset://branding/bevy_bird_dark.png", + size: 1, + nested: Nested { + handle: @"asset://hello.png" + } + } + Transform::from_translation(Vec3::new(1.0, 1.0, 1.0)) + Team::Red { + x: 10, + y: Nested { + foo: 10 + }, + } + Gen:: { + value: 10, + } + on(|event: On| { + }) + Foo(100, @"asset://branding/bevy_bird_dark.png") + [ + (:sprite_big Sprite { size: 2 }), + :widget(bsn_list![Text::new("hi")]), + {sprites}, + ] + } +} + +fn sprite_big() -> impl Scene { + bsn! { + Sprite { size: 100000, handle: "asset://branding/icon.png" } + } +} + +fn x() -> impl Scene { + bsn! { + :"scene://transform_1000.bsn" + Transform { translation: Vec3 { x: 11.0 } } + } +} + +fn transform_1000() -> impl Scene { + bsn! { + Transform { + translation: Vec3 { x: 1000.0, y: 1000.0 } + } + } +} + +fn transform_1337() -> impl Scene { + bsn! { + Transform { + translation: Vec3 { x: 1337.0 } + } + } +} + +#[derive(Component, Debug, GetTemplate)] +struct Sprite { + handle: Handle, + size: usize, + entity: Entity, + nested: Nested, +} + +#[derive(Component, Debug, GetTemplate)] +struct Gen>> { + size: usize, + value: T, +} + +#[derive(Clone, Debug, GetTemplate)] +struct Nested { + foo: usize, + #[template] + handle: Handle, +} + +#[derive(Component, Clone, Debug, GetTemplate)] +struct Foo(usize, #[template] Handle); + +#[derive(Event, EntityEvent)] +struct Explode; + +#[derive(Component, Default, Clone)] +struct Thing; + +fn print( + query: Query<(Entity, Option<&Name>, Option<&ChildOf>, &C), Changed>, +) { + for (e, name, child_of, c) in &query { + println!("Changed {e:?} {name:?} {child_of:?} {c:#?}"); + } +} + +#[derive(Component, Debug, GetTemplate)] +enum Team { + Red { + x: usize, + y: Nested, + }, + Blue, + #[default] + Green(usize, usize), +} + +#[derive(Component, GetTemplate)] +enum Blah { + #[default] + A(Hi), + B(Arrrrg), + C(String), +} + +#[derive(Default)] +struct Arrrrg(Option>); + +impl Clone for Arrrrg { + fn clone(&self) -> Self { + Self(None) + } +} + +impl From for Arrrrg { + fn from(value: F) -> Self { + todo!() + } +} + +#[derive(Clone, Default)] +struct Hi { + size: usize, +} + +fn test() -> impl Scene { + bsn! { + Blah::A(Hi {size: 10}) + Blah::B(|world: &mut World| {}) + Blah::C("hi") + } +} + +fn widget(children: impl SceneList) -> impl Scene { + bsn! { + Node { + width: Val::Px(1.0) + } [ + {children} + ] + } +} diff --git a/examples/scene/ui_scene.rs b/examples/scene/ui_scene.rs new file mode 100644 index 0000000000000..6c8a0e40c6630 --- /dev/null +++ b/examples/scene/ui_scene.rs @@ -0,0 +1,67 @@ +#![allow(unused)] + +//! This example illustrates constructing ui scenes +use bevy::{ + ecs::template::template, + prelude::*, + scene2::prelude::{Scene, SpawnScene, *}, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .run(); +} + +fn setup(world: &mut World) { + world.spawn(Camera2d); + world.spawn_scene(ui()); +} + +fn ui() -> impl Scene { + bsn! { + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + } [ + :button("Button") + ] + } +} + +fn button(label: &'static str) -> impl Scene { + bsn! { + Button + Node { + width: Val::Px(150.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + } + BorderColor::from(Color::BLACK) + BorderRadius::MAX + BackgroundColor(Color::srgb(0.15, 0.15, 0.15)) + on(|event: On>| { + println!("pressed"); + }) + [( + Text(label) + // The `template` wrapper can be used for types that can't implement or don't yet have a template + template(|context| { + Ok(TextFont { + font: context + .resource::() + .load("fonts/FiraSans-Bold.ttf"), + font_size: 33.0, + ..default() + }) + }) + TextColor(Color::srgb(0.9, 0.9, 0.9)) + TextShadow + )] + } +} diff --git a/examples/ui/feathers.rs b/examples/ui/feathers.rs index 2e8a68320ec94..718ca04490439 100644 --- a/examples/ui/feathers.rs +++ b/examples/ui/feathers.rs @@ -2,7 +2,7 @@ use bevy::{ core_widgets::{ - Activate, Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision, + callback, Activate, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision, SliderStep, }, feathers::{ @@ -20,6 +20,7 @@ use bevy::{ InputDispatchPlugin, }, prelude::*, + scene2::prelude::{Scene, *}, ui::{Checked, InteractionDisabled}, winit::WinitSettings, }; @@ -43,25 +44,11 @@ fn main() { fn setup(mut commands: Commands) { // ui camera commands.spawn(Camera2d); - let root = demo_root(&mut commands); - commands.spawn(root); + commands.spawn_scene(demo_root()); } -fn demo_root(commands: &mut Commands) -> impl Bundle { - // Update radio button states based on notification from radio group. - let radio_exclusion = commands.register_system( - |ent: In, q_radio: Query>, mut commands: Commands| { - for radio in q_radio.iter() { - if radio == ent.0 .0 { - commands.entity(radio).insert(Checked); - } else { - commands.entity(radio).remove::(); - } - } - }, - ); - - ( +fn demo_root() -> impl Scene { + bsn! { Node { width: Val::Percent(100.0), height: Val::Percent(100.0), @@ -70,11 +57,10 @@ fn demo_root(commands: &mut Commands) -> impl Bundle { display: Display::Flex, flex_direction: FlexDirection::Column, row_gap: Val::Px(10.0), - ..default() - }, - TabGroup::default(), - ThemeBackgroundColor(tokens::WINDOW_BG), - children![( + } + TabGroup + ThemeBackgroundColor(tokens::WINDOW_BG) + [ Node { display: Display::Flex, flex_direction: FlexDirection::Column, @@ -84,199 +70,153 @@ 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( + } [ + 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::new("Normal") ThemedText)] + ), + ( + :button( ButtonProps { - on_click: Callback::System(commands.register_system( - |_: In| { - info!("Normal button clicked!"); - } - )), + on_click: callback(|_: In| { + info!("Disabled button clicked!"); + }), ..default() }, - (), - Spawn((Text::new("Normal"), ThemedText)) - ), - button( + ) + InteractionDisabled::default() + [(Text::new("Disabled") ThemedText)] + ), + ( + :button( ButtonProps { - on_click: Callback::System(commands.register_system( - |_: In| { - info!("Disabled button clicked!"); - } - )), - ..default() - }, - InteractionDisabled, - Spawn((Text::new("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::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), + } [ + ( + :button(ButtonProps { + on_click: callback(|_: In| { + info!("Left button clicked!"); + }), + corners: RoundedCorners::Left, + ..default() + }) [(Text::new("Left") ThemedText)] + ), + ( + :button(ButtonProps { + on_click: callback(|_: In| { + info!("Center button clicked!"); + }), + corners: RoundedCorners::None, + ..default() + }) [(Text::new("Center") ThemedText)] + ), + ( + :button(ButtonProps { + on_click: callback(|_: In| { + info!("Right button clicked!"); + }), + variant: ButtonVariant::Primary, + corners: RoundedCorners::Right, + }) [(Text::new("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::new("Button") ThemedText)], + ( + :checkbox(CheckboxProps::default()) + Checked::default() + [(Text::new("Checkbox") ThemedText)] ), - checkbox( - CheckboxProps { - on_change: Callback::Ignore, - }, - InteractionDisabled, - Spawn((Text::new("Disabled"), ThemedText)) + ( + :checkbox(CheckboxProps::default()) + InteractionDisabled::default() + [(Text::new("Disabled") ThemedText)] ), - checkbox( - CheckboxProps { - on_change: Callback::Ignore, - }, - (InteractionDisabled, Checked), - Spawn((Text::new("Disabled+Checked"), ThemedText)) + ( + :checkbox(CheckboxProps::default()) + InteractionDisabled + Checked::default() + [(Text::new("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::new("One") ThemedText)], + :radio [(Text::new("Two") ThemedText)], + :radio [(Text::new("Three") ThemedText)], + :radio InteractionDisabled::default() [(Text::new("Disabled") ThemedText)], ] ), - slider( - SliderProps { + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Start, + column_gap: Val::Px(8.0), + } [ + :toggle_switch(ToggleSwitchProps::default()), + :toggle_switch(ToggleSwitchProps::default()) InteractionDisabled, + :toggle_switch(ToggleSwitchProps::default()) InteractionDisabled Checked, + ], + ( + :slider(SliderProps { max: 100.0, value: 20.0, ..default() - }, - (SliderStep(10.), SliderPrecision(2)), + }) + SliderStep(10.) + SliderPrecision(2) ), ] - ),], - ) + ] + } } From ab733198b8b59ec90194797f4a1d46a12043ebfc Mon Sep 17 00:00:00 2001 From: Talin Date: Fri, 18 Jul 2025 15:57:06 -0700 Subject: [PATCH 02/13] Feathers container components in BSN (#34) * Added new "subpane" widget. * Added new "pane" widget. --- crates/bevy_feathers/src/constants.rs | 6 + .../src/containers/flex_spacer.rs | 11 ++ crates/bevy_feathers/src/containers/mod.rs | 8 ++ crates/bevy_feathers/src/containers/pane.rs | 86 +++++++++++++ .../bevy_feathers/src/containers/subpane.rs | 74 +++++++++++ crates/bevy_feathers/src/controls/button.rs | 52 +++++++- crates/bevy_feathers/src/controls/mod.rs | 2 +- crates/bevy_feathers/src/dark_theme.rs | 33 ++++- crates/bevy_feathers/src/lib.rs | 1 + crates/bevy_feathers/src/tokens.rs | 46 +++++++ examples/ui/feathers.rs | 117 +++++++++++++++--- 11 files changed, 413 insertions(+), 23 deletions(-) create mode 100644 crates/bevy_feathers/src/containers/flex_spacer.rs create mode 100644 crates/bevy_feathers/src/containers/mod.rs create mode 100644 crates/bevy_feathers/src/containers/pane.rs create mode 100644 crates/bevy_feathers/src/containers/subpane.rs 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 1bcf2732da1b2..718df066471c6 100644 --- a/crates/bevy_feathers/src/controls/button.rs +++ b/crates/bevy_feathers/src/controls/button.rs @@ -32,6 +32,10 @@ pub enum ButtonVariant { /// A button with a more prominent color, this is used for "call to action" buttons, /// default buttons for dialog boxes, and so on. Primary, + /// For a toggle button, indicates that the button is in a "toggled" state. + Selected, + /// Don't display the button background unless hovering or pressed. + Plain, } /// Parameters for the button template, passed to [`button`] function. @@ -53,6 +57,7 @@ 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.)), @@ -76,6 +81,37 @@ pub fn button(props: ButtonProps) -> impl Scene { } } +/// Tool button scene function: a smaller button for embedding in panel headers. +/// +/// # Arguments +/// * `props` - construction properties for the button. +pub fn tool_button(props: ButtonProps) -> impl Scene { + bsn! { + Node { + height: size::TOOL_HEIGHT, + min_width: size::TOOL_HEIGHT, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + padding: UiRect::axes(Val::Px(2.0), Val::Px(0.)), + } + CoreButton { + on_activate: {props.on_click.clone()}, + } + template_value(props.variant) + template_value(props.corners.to_border_radius(3.0)) + Hovered + // TODO: port CursonIcon to GetTemplate + // CursorIcon::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeBackgroundColor(tokens::BUTTON_BG) + ThemeFontColor(tokens::BUTTON_TEXT) + InheritableFont { + font: fonts::REGULAR, + font_size: 14.0, + } + } +} + fn update_button_styles( q_buttons: Query< ( @@ -160,11 +196,23 @@ fn set_button_colors( (ButtonVariant::Primary, false, true, _) => tokens::BUTTON_PRIMARY_BG_PRESSED, (ButtonVariant::Primary, false, false, true) => tokens::BUTTON_PRIMARY_BG_HOVER, (ButtonVariant::Primary, false, false, false) => tokens::BUTTON_PRIMARY_BG, + (ButtonVariant::Selected, true, _, _) => tokens::BUTTON_SELECTED_BG_DISABLED, + (ButtonVariant::Selected, false, true, _) => tokens::BUTTON_SELECTED_BG_PRESSED, + (ButtonVariant::Selected, false, false, true) => tokens::BUTTON_SELECTED_BG_HOVER, + (ButtonVariant::Selected, false, false, false) => tokens::BUTTON_SELECTED_BG, + (ButtonVariant::Plain, true, _, _) => tokens::BUTTON_PLAIN_BG_DISABLED, + (ButtonVariant::Plain, false, true, _) => tokens::BUTTON_PLAIN_BG_PRESSED, + (ButtonVariant::Plain, false, false, true) => tokens::BUTTON_PLAIN_BG_HOVER, + (ButtonVariant::Plain, false, false, false) => tokens::BUTTON_PLAIN_BG, }; let font_color_token = match (variant, disabled) { - (ButtonVariant::Normal, true) => tokens::BUTTON_TEXT_DISABLED, - (ButtonVariant::Normal, false) => tokens::BUTTON_TEXT, + (ButtonVariant::Normal | ButtonVariant::Selected | ButtonVariant::Plain, true) => { + tokens::BUTTON_TEXT_DISABLED + } + (ButtonVariant::Normal | ButtonVariant::Selected | ButtonVariant::Plain, false) => { + tokens::BUTTON_TEXT + } (ButtonVariant::Primary, true) => tokens::BUTTON_PRIMARY_TEXT_DISABLED, (ButtonVariant::Primary, false) => tokens::BUTTON_PRIMARY_TEXT, }; diff --git a/crates/bevy_feathers/src/controls/mod.rs b/crates/bevy_feathers/src/controls/mod.rs index ecad39707b925..5c35cb610c822 100644 --- a/crates/bevy_feathers/src/controls/mod.rs +++ b/crates/bevy_feathers/src/controls/mod.rs @@ -7,7 +7,7 @@ mod radio; mod slider; mod toggle_switch; -pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant}; +pub use button::{button, tool_button, ButtonPlugin, ButtonProps, ButtonVariant}; pub use checkbox::{checkbox, CheckboxPlugin, CheckboxProps}; pub use radio::{radio, RadioPlugin}; pub use slider::{slider, SliderPlugin, SliderProps}; diff --git a/crates/bevy_feathers/src/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/lib.rs b/crates/bevy_feathers/src/lib.rs index ab02304a85b30..b07e4ea6e1583 100644 --- a/crates/bevy_feathers/src/lib.rs +++ b/crates/bevy_feathers/src/lib.rs @@ -31,6 +31,7 @@ use crate::{ }; pub mod constants; +pub mod containers; pub mod controls; pub mod cursor; pub mod dark_theme; diff --git a/crates/bevy_feathers/src/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/examples/ui/feathers.rs b/examples/ui/feathers.rs index 718ca04490439..ccf138f848819 100644 --- a/examples/ui/feathers.rs +++ b/examples/ui/feathers.rs @@ -1,14 +1,19 @@ //! This example shows off the various Bevy Feathers widgets. use bevy::{ + color::palettes, core_widgets::{ callback, Activate, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision, SliderStep, }, feathers::{ + containers::{ + flex_spacer, pane, pane_body, pane_header, pane_header_divider, subpane, subpane_body, + subpane_header, + }, controls::{ - button, checkbox, radio, slider, toggle_switch, ButtonProps, ButtonVariant, - CheckboxProps, SliderProps, ToggleSwitchProps, + button, checkbox, radio, slider, toggle_switch, tool_button, ButtonProps, + ButtonVariant, CheckboxProps, SliderProps, ToggleSwitchProps, }, dark_theme::create_dark_theme, rounded_corners::RoundedCorners, @@ -55,8 +60,8 @@ fn demo_root() -> impl Scene { align_items: AlignItems::Start, justify_content: JustifyContent::Start, display: Display::Flex, - flex_direction: FlexDirection::Column, - row_gap: Val::Px(10.0), + flex_direction: FlexDirection::Row, + column_gap: Val::Px(10.0), } TabGroup ThemeBackgroundColor(tokens::WINDOW_BG) @@ -84,7 +89,7 @@ fn demo_root() -> impl Scene { info!("Normal button clicked!"); }), ..default() - }) [(Text::new("Normal") ThemedText)] + }) [(Text("Normal") ThemedText)] ), ( :button( @@ -96,7 +101,7 @@ fn demo_root() -> impl Scene { }, ) InteractionDisabled::default() - [(Text::new("Disabled") ThemedText)] + [(Text("Disabled") ThemedText)] ), ( :button( @@ -107,7 +112,7 @@ fn demo_root() -> impl Scene { variant: ButtonVariant::Primary, ..default() }, - ) [(Text::new("Primary") ThemedText)] + ) [(Text("Primary") ThemedText)] ), ], Node { @@ -124,7 +129,7 @@ fn demo_root() -> impl Scene { }), corners: RoundedCorners::Left, ..default() - }) [(Text::new("Left") ThemedText)] + }) [(Text("Left") ThemedText)] ), ( :button(ButtonProps { @@ -133,7 +138,7 @@ fn demo_root() -> impl Scene { }), corners: RoundedCorners::None, ..default() - }) [(Text::new("Center") ThemedText)] + }) [(Text("Center") ThemedText)] ), ( :button(ButtonProps { @@ -142,7 +147,7 @@ fn demo_root() -> impl Scene { }), variant: ButtonVariant::Primary, corners: RoundedCorners::Right, - }) [(Text::new("Right") ThemedText)] + }) [(Text("Right") ThemedText)] ), ], :button( @@ -152,22 +157,22 @@ fn demo_root() -> impl Scene { }), ..default() } - ) [(Text::new("Button") ThemedText)], + ) [(Text("Button") ThemedText)], ( :checkbox(CheckboxProps::default()) Checked::default() - [(Text::new("Checkbox") ThemedText)] + [(Text("Checkbox") ThemedText)] ), ( :checkbox(CheckboxProps::default()) InteractionDisabled::default() - [(Text::new("Disabled") ThemedText)] + [(Text("Disabled") ThemedText)] ), ( :checkbox(CheckboxProps::default()) InteractionDisabled Checked::default() - [(Text::new("Disabled+Checked") ThemedText)] + [(Text("Disabled+Checked") ThemedText)] ), ( Node { @@ -190,10 +195,10 @@ fn demo_root() -> impl Scene { ), } [ - :radio Checked::default() [(Text::new("One") ThemedText)], - :radio [(Text::new("Two") ThemedText)], - :radio [(Text::new("Three") ThemedText)], - :radio InteractionDisabled::default() [(Text::new("Disabled") ThemedText)], + :radio Checked::default() [(Text("One") ThemedText)], + :radio [(Text("Two") ThemedText)], + :radio [(Text("Three") ThemedText)], + :radio InteractionDisabled::default() [(Text("Disabled") ThemedText)], ] ), Node { @@ -216,6 +221,82 @@ fn demo_root() -> impl Scene { SliderStep(10.) SliderPrecision(2) ), + ], + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + align_items: AlignItems::Stretch, + justify_content: JustifyContent::Start, + padding: UiRect::all(Val::Px(8.0)), + row_gap: Val::Px(8.0), + width: Val::Percent(30.), + min_width: Val::Px(200.), + } [ + ( + :subpane [ + :subpane_header [ + (Text("Left") ThemedText), + (Text("Center") ThemedText), + (Text("Right") ThemedText) + ], + :subpane_body [ + (Text("Body") ThemedText), + ], + ] + ), + ( + :pane [ + :pane_header [ + :tool_button(ButtonProps { + variant: ButtonVariant::Selected, + ..default() + }) [ + (Text("\u{0398}") ThemedText) + ], + :pane_header_divider, + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{00BC}") ThemedText) + ], + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{00BD}") ThemedText) + ], + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{00BE}") ThemedText) + ], + :pane_header_divider, + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{20AC}") ThemedText) + ], + :flex_spacer, + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{00D7}") ThemedText) + ], + ], + ( + :pane_body [ + (Text("Some") ThemedText), + (Text("Content") ThemedText), + (Text("Here") ThemedText), + ] + BackgroundColor(palettes::tailwind::EMERALD_800) + ), + ] + ) ] ] } From e4a617059b2a525c228729ed44e87ab7889983a5 Mon Sep 17 00:00:00 2001 From: Viktor Gustavsson Date: Thu, 31 Jul 2025 01:03:04 +0200 Subject: [PATCH 03/13] Fix `rustfmt` in `codegen.rs` (#39) * Insert turbofish line breaks to satisfy rustfmt * Format codegen.rs --- crates/bevy_scene2/macros/src/bsn/codegen.rs | 297 ++++++++++--------- 1 file changed, 165 insertions(+), 132 deletions(-) diff --git a/crates/bevy_scene2/macros/src/bsn/codegen.rs b/crates/bevy_scene2/macros/src/bsn/codegen.rs index 0b29c88c0247a..013ac796c2139 100644 --- a/crates/bevy_scene2/macros/src/bsn/codegen.rs +++ b/crates/bevy_scene2/macros/src/bsn/codegen.rs @@ -1,5 +1,6 @@ use crate::bsn::types::{ - Bsn, BsnConstructor, BsnEntry, BsnFields, BsnInheritedScene, BsnRelatedSceneList, BsnRoot, BsnSceneListItem, BsnSceneListItems, BsnType, BsnValue + Bsn, BsnConstructor, BsnEntry, BsnFields, BsnInheritedScene, BsnRelatedSceneList, BsnRoot, + BsnSceneListItem, BsnSceneListItems, BsnType, BsnValue, }; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; @@ -16,107 +17,129 @@ impl Bsn { 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::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::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::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::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::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::GetTemplateConstructor(BsnConstructor {type_path, function, args})=> { - quote! { - <#type_path as #bevy_scene::PatchGetTemplate>::patch( - move |value| { - *value = <#type_path as #bevy_ecs::template::GetTemplate>::Template::#function(#args); - } - ) - } + } + BsnEntry::ChildrenSceneList(scene_list) => { + let scenes = scene_list.0.to_tokens(bevy_scene, bevy_ecs, bevy_asset); + quote! { + #bevy_scene::RelatedScenes::<#bevy_ecs::hierarchy::ChildOf, _>::new(#scenes) } - BsnEntry::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::RelatedSceneList(BsnRelatedSceneList { scene_list, relationship_path }) => { - let scenes = scene_list.0.to_tokens(bevy_scene, bevy_ecs, bevy_asset); - quote! { - #bevy_scene::RelatedScenes::<<#relationship_path as #bevy_ecs::relationship::RelationshipTarget>::Relationship, _>::new( - #scenes - ) - } + } + BsnEntry::InheritedScene(inherited_scene) => match inherited_scene { + BsnInheritedScene::Asset(lit_str) => { + quote! {#bevy_scene::InheritSceneAsset::from(#lit_str)} } - 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))}, + 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::Name(ident) => { + let name = ident.to_string(); + quote! { + <#bevy_ecs::name::Name as PatchGetTemplate>::patch( + move |value| { + *value = Name(#name.into()); + } + ) } - + } }); } @@ -148,8 +171,8 @@ impl BsnType { 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}); + 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); @@ -160,14 +183,21 @@ impl BsnType { let value = &f.value; if let Some(BsnValue::Type(bsn_type)) = &value { if bsn_type.enum_variant.is_some() { - quote!{*#name = #bsn_type;} + 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)*} + 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;} + quote! {*#name = #value;} } }); let field_names = fields.iter().map(|f| &f.name); @@ -187,17 +217,27 @@ impl BsnType { let value = &f.value; if let BsnValue::Type(bsn_type) = &value { if bsn_type.enum_variant.is_some() { - quote!{*#name = #bsn_type;} + 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)*} + 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;} + quote! {*#name = #value;} } }); - let field_names = fields.iter().enumerate().map(|(index, _)| format_ident!("t{}", index)); + 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(); @@ -264,9 +304,8 @@ impl BsnType { } BsnValue::Type(field_type) => { if field_type.enum_variant.is_some() { - assignments.push( - quote! {#(#field_path.)*#field_index = #field_type;}, - ); + 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)); @@ -290,16 +329,13 @@ impl BsnType { 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}, - }); + 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,)*) } @@ -310,50 +346,47 @@ 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}) + Some(quote! {::#variant}) } else { None }; let result = match &self.fields { BsnFields::Named(fields) => { - let assignments =fields.iter().map(|f| { - let name= &f.name; + let assignments = fields.iter().map(|f| { + let name = &f.name; let value = &f.value; - quote!{#name: #value} + quote! {#name: #value} }); - quote!{ + quote! { #path #maybe_variant { #(#assignments,)* } } - }, + } BsnFields::Tuple(fields) => { - let assignments =fields.iter().map(|f| { - &f.value - }); - quote!{ + 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); + quote! {{#expr_tokens}.into()}.to_tokens(tokens); } BsnValue::Closure(closure_tokens) => { - quote!{(#closure_tokens).into()}.to_tokens(tokens); + quote! {(#closure_tokens).into()}.to_tokens(tokens); } BsnValue::Ident(ident) => { - quote!{(#ident).into()}.to_tokens(tokens); + quote! {(#ident).into()}.to_tokens(tokens); } BsnValue::Lit(lit) => match lit { Lit::Str(str) => quote! {#str.into()}.to_tokens(tokens), @@ -362,7 +395,7 @@ impl ToTokens for BsnValue { BsnValue::Tuple(tuple) => { let tuple_tokens = tuple.0.iter(); quote! {(#(#tuple_tokens),*)}.to_tokens(tokens); - }, + } BsnValue::Type(ty) => { ty.to_tokens(tokens); } From fbae07418952bd12efecb7e220b2dc8e6f94741e Mon Sep 17 00:00:00 2001 From: Viktor Gustavsson Date: Thu, 31 Jul 2025 01:06:29 +0200 Subject: [PATCH 04/13] Add name expression BSN syntax (#38) Co-authored-by: Carter Anderson --- crates/bevy_scene2/macros/src/bsn/codegen.rs | 9 +++++++++ crates/bevy_scene2/macros/src/bsn/parse.rs | 6 +++++- crates/bevy_scene2/macros/src/bsn/types.rs | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/bevy_scene2/macros/src/bsn/codegen.rs b/crates/bevy_scene2/macros/src/bsn/codegen.rs index 013ac796c2139..59d001a4c7cd2 100644 --- a/crates/bevy_scene2/macros/src/bsn/codegen.rs +++ b/crates/bevy_scene2/macros/src/bsn/codegen.rs @@ -140,6 +140,15 @@ impl Bsn { ) } } + BsnEntry::NameExpression(expr_tokens) => { + quote! { + <#bevy_ecs::name::Name as PatchGetTemplate>::patch( + move |value| { + *value = Name({#expr_tokens}.into()); + } + ) + } + } }); } diff --git a/crates/bevy_scene2/macros/src/bsn/parse.rs b/crates/bevy_scene2/macros/src/bsn/parse.rs index 0306f763642e0..f0eafb15fbb1a 100644 --- a/crates/bevy_scene2/macros/src/bsn/parse.rs +++ b/crates/bevy_scene2/macros/src/bsn/parse.rs @@ -73,7 +73,11 @@ impl Parse for BsnEntry { BsnEntry::InheritedScene(input.parse::()?) } else if input.peek(Token![#]) { input.parse::()?; - BsnEntry::Name(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) { diff --git a/crates/bevy_scene2/macros/src/bsn/types.rs b/crates/bevy_scene2/macros/src/bsn/types.rs index 81c1484a7a553..6a64b3ad129a9 100644 --- a/crates/bevy_scene2/macros/src/bsn/types.rs +++ b/crates/bevy_scene2/macros/src/bsn/types.rs @@ -12,6 +12,7 @@ pub struct Bsn { #[derive(Debug)] pub enum BsnEntry { Name(Ident), + NameExpression(TokenStream), GetTemplatePatch(BsnType), TemplatePatch(BsnType), GetTemplateConstructor(BsnConstructor), From 40e6f12bbad44dfe2566fda2ead83469d53717d0 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 1 Aug 2025 00:28:22 +0200 Subject: [PATCH 05/13] Don't assume entity still exists for `ScenePatch` `AssetEvent` (#40) --- crates/bevy_scene2/src/spawn.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/bevy_scene2/src/spawn.rs b/crates/bevy_scene2/src/spawn.rs index e40c8e773c7fe..70aa5ff5f91f9 100644 --- a/crates/bevy_scene2/src/spawn.rs +++ b/crates/bevy_scene2/src/spawn.rs @@ -96,8 +96,9 @@ pub fn spawn_queued( }; for entity in entities { - let mut entity_mut = world.get_entity_mut(entity).unwrap(); - scene.spawn(&mut entity_mut).unwrap(); + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + scene.spawn(&mut entity_mut).unwrap(); + } } } } From 62e3c6e762c7f7e24720e49d5beebdd8e2f9f44c Mon Sep 17 00:00:00 2001 From: Viktor Gustavsson Date: Sun, 3 Aug 2025 23:05:34 +0200 Subject: [PATCH 06/13] Restore `bevy_feathers` features after merge with upstream (#41) * Restore the panes in the feathers example * Migrate tool_button to use EntityCursor * Remove unused imports * Fix AlphaPattern with bsn! and simplify handle initialization --- crates/bevy_feathers/src/alpha_pattern.rs | 57 +++++--------- crates/bevy_feathers/src/controls/button.rs | 4 +- .../src/controls/color_swatch.rs | 11 +-- crates/bevy_feathers/src/controls/mod.rs | 3 - .../src/controls/toggle_switch.rs | 2 +- crates/bevy_feathers/src/cursor.rs | 1 - crates/bevy_feathers/src/lib.rs | 4 +- examples/ui/feathers.rs | 78 ++++++++++++++++++- 8 files changed, 101 insertions(+), 59 deletions(-) diff --git a/crates/bevy_feathers/src/alpha_pattern.rs b/crates/bevy_feathers/src/alpha_pattern.rs index 5401006a49176..4f70346535055 100644 --- a/crates/bevy_feathers/src/alpha_pattern.rs +++ b/crates/bevy_feathers/src/alpha_pattern.rs @@ -1,13 +1,5 @@ -use bevy_app::Plugin; -use bevy_asset::{Asset, Assets, Handle}; -use bevy_ecs::{ - component::Component, - lifecycle::Add, - observer::On, - resource::Resource, - system::{Query, Res}, - world::FromWorld, -}; +use bevy_asset::{Asset, Assets}; +use bevy_ecs::{component::Component, lifecycle::HookContext, world::DeferredWorld}; use bevy_reflect::TypePath; use bevy_render::render_resource::{AsBindGroup, ShaderRef}; use bevy_ui_render::ui_material::{MaterialNode, UiMaterial}; @@ -21,39 +13,28 @@ impl UiMaterial for AlphaPatternMaterial { } } -#[derive(Resource)] -pub(crate) struct AlphaPatternResource(pub(crate) Handle); - -impl FromWorld for AlphaPatternResource { - fn from_world(world: &mut bevy_ecs::world::World) -> Self { - let mut ui_materials = world - .get_resource_mut::>() - .unwrap(); - Self(ui_materials.add(AlphaPatternMaterial::default())) - } -} - /// Marker that tells us we want to fill in the [`MaterialNode`] with the alpha material. #[derive(Component, Default, Clone)] +#[require(MaterialNode)] +#[component(on_add = on_add_alpha_pattern)] pub(crate) struct AlphaPattern; /// Observer to fill in the material handle (since we don't have access to the materials asset /// in the template) -fn on_add_alpha_pattern( - ev: On, - mut q_material_node: Query<&mut MaterialNode>, - r_material: Res, -) { - if let Ok(mut material) = q_material_node.get_mut(ev.target()) { - material.0 = r_material.0.clone(); - } -} - -/// Plugin which registers the systems for updating the button styles. -pub struct AlphaPatternPlugin; - -impl Plugin for AlphaPatternPlugin { - fn build(&self, app: &mut bevy_app::App) { - app.add_observer(on_add_alpha_pattern); +fn on_add_alpha_pattern(mut world: DeferredWorld, context: HookContext) { + let mut materials = world.resource_mut::>(); + + let handle = if materials.is_empty() { + materials.add(AlphaPatternMaterial::default()) + } else { + let id = materials.iter().next().unwrap().0; + materials.get_strong_handle(id).unwrap() + }; + + if let Some(mut material) = world + .entity_mut(context.entity) + .get_mut::>() + { + material.0 = handle; } } diff --git a/crates/bevy_feathers/src/controls/button.rs b/crates/bevy_feathers/src/controls/button.rs index ad08b9b40ca9d..70b28df47790b 100644 --- a/crates/bevy_feathers/src/controls/button.rs +++ b/crates/bevy_feathers/src/controls/button.rs @@ -21,7 +21,6 @@ use crate::{ tokens, }; use bevy_input_focus::tab_navigation::TabIndex; -use bevy_winit::cursor::CursorIcon; /// Color variants for buttons. This also functions as a component used by the dynamic styling /// system to identify which entities are buttons. @@ -100,8 +99,7 @@ pub fn tool_button(props: ButtonProps) -> impl Scene { template_value(props.variant) template_value(props.corners.to_border_radius(3.0)) Hovered - // TODO: port CursonIcon to GetTemplate - // CursorIcon::System(bevy_window::SystemCursorIcon::Pointer) + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer) TabIndex(0) ThemeBackgroundColor(tokens::BUTTON_BG) ThemeFontColor(tokens::BUTTON_TEXT) diff --git a/crates/bevy_feathers/src/controls/color_swatch.rs b/crates/bevy_feathers/src/controls/color_swatch.rs index b1f7226793279..cda4a2a8d3708 100644 --- a/crates/bevy_feathers/src/controls/color_swatch.rs +++ b/crates/bevy_feathers/src/controls/color_swatch.rs @@ -1,15 +1,9 @@ -use bevy_asset::Handle; use bevy_color::Alpha; use bevy_ecs::component::Component; +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 bevy_scene2::{bsn, Scene}; +use crate::{alpha_pattern::AlphaPattern, constants::size, palette}; /// Marker identifying a color swatch. #[derive(Component, Default, Clone)] @@ -33,7 +27,6 @@ pub fn color_swatch() -> impl Scene { } ColorSwatch AlphaPattern - MaterialNode::(Handle::default()) BorderRadius::all(Val::Px(5.0)) [ Node { diff --git a/crates/bevy_feathers/src/controls/mod.rs b/crates/bevy_feathers/src/controls/mod.rs index d9c11ae344909..29a31deb9309f 100644 --- a/crates/bevy_feathers/src/controls/mod.rs +++ b/crates/bevy_feathers/src/controls/mod.rs @@ -15,15 +15,12 @@ pub use radio::{radio, RadioPlugin}; pub use slider::{slider, SliderPlugin, SliderProps}; pub use toggle_switch::{toggle_switch, ToggleSwitchPlugin, ToggleSwitchProps}; -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, RadioPlugin, diff --git a/crates/bevy_feathers/src/controls/toggle_switch.rs b/crates/bevy_feathers/src/controls/toggle_switch.rs index 3c4a7d8981b62..95abb4e7aa522 100644 --- a/crates/bevy_feathers/src/controls/toggle_switch.rs +++ b/crates/bevy_feathers/src/controls/toggle_switch.rs @@ -1,7 +1,7 @@ use accesskit::Role; use bevy_a11y::AccessibilityNode; use bevy_app::{Plugin, PreUpdate}; -use bevy_core_widgets::{Callback, CallbackTemplate, CoreCheckbox, ValueChange}; +use bevy_core_widgets::{CallbackTemplate, CoreCheckbox, ValueChange}; use bevy_ecs::{ component::Component, entity::Entity, diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs index 2e23a27f935f6..87a0e93f23063 100644 --- a/crates/bevy_feathers/src/cursor.rs +++ b/crates/bevy_feathers/src/cursor.rs @@ -9,7 +9,6 @@ use bevy_ecs::{ resource::Resource, schedule::IntoScheduleConfigs, system::{Commands, Query, Res}, - template::GetTemplate, VariantDefaults, }; use bevy_picking::{hover::HoverMap, pointer::PointerId, PickingSystems}; diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs index e35ea600b0fb1..56e7af02e20ec 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}, @@ -78,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/examples/ui/feathers.rs b/examples/ui/feathers.rs index f40800fdae2b8..c3e709e8cf7dd 100644 --- a/examples/ui/feathers.rs +++ b/examples/ui/feathers.rs @@ -12,7 +12,7 @@ use bevy::{ subpane_header, }, controls::{ - button, checkbox, color_swatch, radio, slider, toggle_switch, ButtonProps, + button, checkbox, color_swatch, radio, slider, toggle_switch, tool_button, ButtonProps, ButtonVariant, CheckboxProps, SliderProps, ToggleSwitchProps, }, dark_theme::create_dark_theme, @@ -222,6 +222,82 @@ fn demo_root() -> impl Scene { SliderPrecision(2) ), 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.), + } [ + ( + :subpane [ + :subpane_header [ + (Text("Left") ThemedText), + (Text("Center") ThemedText), + (Text("Right") ThemedText) + ], + :subpane_body [ + (Text("Body") ThemedText), + ], + ] + ), + ( + :pane [ + :pane_header [ + :tool_button(ButtonProps { + variant: ButtonVariant::Selected, + ..default() + }) [ + (Text("\u{0398}") ThemedText) + ], + :pane_header_divider, + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{00BC}") ThemedText) + ], + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{00BD}") ThemedText) + ], + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{00BE}") ThemedText) + ], + :pane_header_divider, + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{20AC}") ThemedText) + ], + :flex_spacer, + :tool_button(ButtonProps{ + variant: ButtonVariant::Plain, + ..default() + }) [ + (Text("\u{00D7}") ThemedText) + ], + ], + ( + :pane_body [ + (Text("Some") ThemedText), + (Text("Content") ThemedText), + (Text("Here") ThemedText), + ] + BackgroundColor(palettes::tailwind::EMERALD_800) + ), + ] + ) ] ] } From 7117b031953f59440fb42bd79412189327038705 Mon Sep 17 00:00:00 2001 From: Tim Blackbird Date: Sun, 10 Aug 2025 04:35:08 +0200 Subject: [PATCH 07/13] Fix `scene_spawn` in template (#42) --- crates/bevy_scene2/src/lib.rs | 4 ++- crates/bevy_scene2/src/spawn.rs | 56 ++++++++++++++++++++------------- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/crates/bevy_scene2/src/lib.rs b/crates/bevy_scene2/src/lib.rs index cac0e9b521597..968e449fa2525 100644 --- a/crates/bevy_scene2/src/lib.rs +++ b/crates/bevy_scene2/src/lib.rs @@ -32,8 +32,10 @@ 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_systems(Update, (resolve_scene_patches, spawn_queued).chain()) + .add_observer(on_add_scene_patch_instance); } } diff --git a/crates/bevy_scene2/src/spawn.rs b/crates/bevy_scene2/src/spawn.rs index 44ec198599211..775bd6309d3de 100644 --- a/crates/bevy_scene2/src/spawn.rs +++ b/crates/bevy_scene2/src/spawn.rs @@ -62,39 +62,51 @@ pub struct QueuedScenes { waiting_entities: HashMap, Vec>, } +#[derive(Resource, Default)] +pub struct NewScenes { + entities: Vec, +} + +pub fn on_add_scene_patch_instance( + add: On, + mut new_scenes: ResMut, +) { + new_scenes.entities.push(add.entity); +} + pub fn spawn_queued( world: &mut World, - handles: &mut QueryState<(Entity, &ScenePatchInstance), Added>, + 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>>| { - for (entity, id) in handles - .iter(world) - .map(|(e, h)| (e, h.id())) - .collect::>() - { - if let Some(scene) = patches.get_mut(id).and_then(|p| p.resolved.as_mut()) { - let mut entity_mut = world.get_entity_mut(entity).unwrap(); - scene.spawn(&mut entity_mut).unwrap(); - } else { - let entities = queued.waiting_entities.entry(id).or_default(); - entities.push(entity); + 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()) - else { - continue; - }; - - let Some(entities) = queued.waiting_entities.remove(id) else { - continue; - }; - + 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(); From ecab5a106f6a78de45622129115869907c988eb0 Mon Sep 17 00:00:00 2001 From: Talin Date: Fri, 10 Oct 2025 16:21:32 -0700 Subject: [PATCH 08/13] Fix for slider initialization in bsn feathers. (#45) --- crates/bevy_feathers/src/controls/color_slider.rs | 4 ++-- examples/ui/feathers.rs | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/bevy_feathers/src/controls/color_slider.rs b/crates/bevy_feathers/src/controls/color_slider.rs index ad8b613b98d89..1dd1631d9906a 100644 --- a/crates/bevy_feathers/src/controls/color_slider.rs +++ b/crates/bevy_feathers/src/controls/color_slider.rs @@ -283,10 +283,10 @@ fn update_slider_pos( q_children: Query<&Children>, mut q_slider_thumb: Query<&mut Node, With>, ) { - for (slider_ent, value, range) in q_sliders.iter_mut() { + for (slider_ent, SliderValue(value), range) in q_sliders.iter_mut() { for child in q_children.iter_descendants(slider_ent) { if let Ok(mut thumb_node) = q_slider_thumb.get_mut(child) { - thumb_node.left = Val::Percent(range.thumb_position(value.0) * 100.0); + thumb_node.left = Val::Percent(range.thumb_position(*value) * 100.0); } } } diff --git a/examples/ui/feathers.rs b/examples/ui/feathers.rs index 59520af535a32..dae81bfef153c 100644 --- a/examples/ui/feathers.rs +++ b/examples/ui/feathers.rs @@ -368,11 +368,14 @@ fn demo_root() -> impl Scene { fn update_colors( colors: Res, - mut sliders: Query<(Entity, &ColorSlider, &mut SliderBaseColor)>, - swatches: Query<(&SwatchType, &Children), With>, + mut sliders: Query<(Entity, Ref, &mut SliderBaseColor)>, + swatches: Query<(Ref, &Children), With>, mut commands: Commands, ) { - if colors.is_changed() { + // Check to see if sliders or swatches were added after resource has changed. + let sliders_added = sliders.iter().any(|(_, slider, _)| slider.is_added()); + let swatches_added = swatches.iter().any(|(swatch, _)| swatch.is_added()); + if colors.is_changed() || sliders_added || swatches_added { for (slider_ent, slider, mut base) in sliders.iter_mut() { match slider.channel { ColorChannel::Red => { @@ -423,7 +426,7 @@ fn update_colors( for (swatch_type, children) in swatches.iter() { commands .entity(children[0]) - .insert(BackgroundColor(match swatch_type { + .insert(BackgroundColor(match *swatch_type { SwatchType::Rgb => colors.rgb_color.into(), SwatchType::Hsl => colors.hsl_color.into(), })); From 0c7807307d1e5668370c12b6b338b2d8306fc178 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 13 Oct 2025 17:44:53 -0700 Subject: [PATCH 09/13] Add spawn_related_scenes --- crates/bevy_scene2/src/lib.rs | 1 + crates/bevy_scene2/src/scene_patch.rs | 27 +++- crates/bevy_scene2/src/spawn.rs | 184 +++++++++++++++++++++----- 3 files changed, 177 insertions(+), 35 deletions(-) diff --git a/crates/bevy_scene2/src/lib.rs b/crates/bevy_scene2/src/lib.rs index 968e449fa2525..318b805582006 100644 --- a/crates/bevy_scene2/src/lib.rs +++ b/crates/bevy_scene2/src/lib.rs @@ -34,6 +34,7 @@ impl Plugin for ScenePlugin { app.init_resource::() .init_resource::() .init_asset::() + .init_asset::() .add_systems(Update, (resolve_scene_patches, spawn_queued).chain()) .add_observer(on_add_scene_patch_instance); } diff --git a/crates/bevy_scene2/src/scene_patch.rs b/crates/bevy_scene2/src/scene_patch.rs index cc5c090feaeac..d261b969262b7 100644 --- a/crates/bevy_scene2/src/scene_patch.rs +++ b/crates/bevy_scene2/src/scene_patch.rs @@ -1,4 +1,4 @@ -use crate::{ResolvedScene, Scene}; +use crate::{ResolvedScene, Scene, SceneList}; use bevy_asset::{Asset, AssetServer, Handle, UntypedHandle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::component::Component; @@ -31,3 +31,28 @@ impl ScenePatch { #[derive(Component, Deref, DerefMut)] pub struct ScenePatchInstance(pub Handle); + +#[derive(Asset, TypePath)] +pub struct SceneListPatch { + pub patch: Box, + #[dependency] + pub dependencies: Vec, + // TODO: consider breaking this out to prevent mutating asset events when resolved + pub resolved: Option>, +} + +impl SceneListPatch { + pub fn load(assets: &AssetServer, scene_list: L) -> Self { + let mut dependencies = Vec::new(); + scene_list.register_dependencies(&mut dependencies); + let dependencies = dependencies + .iter() + .map(|i| assets.load::(i.clone()).untyped()) + .collect::>(); + SceneListPatch { + patch: Box::new(scene_list), + dependencies, + resolved: None, + } + } +} diff --git a/crates/bevy_scene2/src/spawn.rs b/crates/bevy_scene2/src/spawn.rs index 775bd6309d3de..134757eb3102f 100644 --- a/crates/bevy_scene2/src/spawn.rs +++ b/crates/bevy_scene2/src/spawn.rs @@ -1,12 +1,16 @@ -use crate::{ResolvedScene, Scene, ScenePatch, ScenePatchInstance}; -use bevy_asset::{AssetEvent, AssetId, AssetServer, Assets}; -use bevy_ecs::{message::MessageCursor, prelude::*}; +use crate::{ResolvedScene, Scene, SceneList, SceneListPatch, ScenePatch, ScenePatchInstance}; +use bevy_asset::{AssetEvent, AssetId, AssetServer, Assets, Handle}; +use bevy_ecs::{message::MessageCursor, prelude::*, relationship::Relationship}; use bevy_platform::collections::HashMap; pub trait SpawnScene { fn spawn_scene(&mut self, scene: S) -> EntityWorldMut<'_>; } +pub trait SpawnRelatedScenes { + fn spawn_related_scenes(self, scenes: impl SceneList) -> Self; +} + impl SpawnScene for World { fn spawn_scene(&mut self, scene: S) -> EntityWorldMut<'_> { let assets = self.resource::(); @@ -16,6 +20,37 @@ impl SpawnScene for World { } } +impl SpawnRelatedScenes for EntityWorldMut<'_> { + fn spawn_related_scenes(mut self, scenes: impl SceneList) -> Self { + let assets = self.resource::(); + let patch = SceneListPatch::load(assets, scenes); + let handle = assets.add(patch); + let entity = self.id(); + self.resource_mut::().scene_entities.push(( + SceneListSpawn { + entity, + insert: |entity, target| { + entity.insert( + <::Relationship as Relationship>::from(target), + ); + }, + }, + handle, + )); + self + } +} + +impl SpawnRelatedScenes for EntityCommands<'_> { + fn spawn_related_scenes(mut self, scenes: impl SceneList) -> Self { + self.queue(move |entity: EntityWorldMut| { + entity.spawn_related_scenes::(scenes); + }); + + self + } +} + pub trait CommandsSpawnScene { fn spawn_scene(&mut self, scene: S) -> EntityCommands<'_>; } @@ -38,8 +73,10 @@ impl<'w, 's> CommandsSpawnScene for Commands<'w, 's> { pub fn resolve_scene_patches( mut events: MessageReader>, + mut list_events: MessageReader>, assets: Res, mut patches: ResMut>, + mut list_patches: ResMut>, ) { for event in events.read() { match *event { @@ -55,16 +92,37 @@ pub fn resolve_scene_patches( _ => {} } } + for event in list_events.read() { + match *event { + // TODO: handle modified? + AssetEvent::LoadedWithDependencies { id } => { + let list_patch = list_patches.get_mut(id).unwrap(); + let mut scenes = Vec::new(); + // TODO: real error handling + list_patch.patch.patch_list(&assets, &patches, &mut scenes); + println!("LOADED {}", scenes.len()); + list_patch.resolved = Some(scenes); + } + _ => {} + } + } } #[derive(Resource, Default)] pub struct QueuedScenes { waiting_entities: HashMap, Vec>, + waiting_list_entities: HashMap, Vec>, +} + +struct SceneListSpawn { + entity: Entity, + insert: fn(&mut EntityWorldMut, target: Entity), } #[derive(Resource, Default)] pub struct NewScenes { entities: Vec, + scene_entities: Vec<(SceneListSpawn, Handle)>, } pub fn on_add_scene_patch_instance( @@ -78,42 +136,100 @@ pub fn spawn_queued( world: &mut World, handles: &mut QueryState<&ScenePatchInstance>, mut reader: Local>>, + mut list_reader: Local>>, ) { world.resource_scope(|world, mut patches: Mut>| { - world.resource_scope(|world, mut 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); + world.resource_scope(|world, mut list_patches: Mut>| { + world.resource_scope(|world, mut queued: Mut| { + world.resource_scope(|world, events: Mut>>| { + world.resource_scope( + |world, list_events: Mut>>| { + loop { + let mut new_scenes = world.resource_mut::(); + if new_scenes.entities.is_empty() { + break; + } + for entity in core::mem::take(&mut new_scenes.entities) { + if let Ok(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); + } + } + } + } + loop { + let mut new_scenes = world.resource_mut::(); + if new_scenes.scene_entities.is_empty() { + break; + } + for (scene_list_spawn, handle) in + core::mem::take(&mut new_scenes.scene_entities) + { + if let Some(resolved_scenes) = list_patches + .get_mut(&handle) + .and_then(|p| p.resolved.as_mut()) + { + for scene in resolved_scenes { + let mut child_entity = world.spawn_empty(); + (scene_list_spawn.insert)( + &mut child_entity, + scene_list_spawn.entity, + ); + scene.spawn(&mut child_entity).unwrap(); + } + } else { + let entities = queued + .waiting_list_entities + .entry(handle.id()) + .or_default(); + entities.push(scene_list_spawn); + } + } } - } - } - } - 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(); + 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(); + } + } + } + } + for event in list_reader.read(&list_events) { + if let AssetEvent::LoadedWithDependencies { id } = event + && let Some(resolved_scenes) = + list_patches.get_mut(*id).and_then(|p| p.resolved.as_mut()) + && let Some(scene_list_spawns) = + queued.waiting_list_entities.remove(id) + { + for scene_list_spawn in scene_list_spawns { + for scene in resolved_scenes.iter_mut() { + let mut child_entity = world.spawn_empty(); + (scene_list_spawn.insert)( + &mut child_entity, + scene_list_spawn.entity, + ); + scene.spawn(&mut child_entity).unwrap(); + } + } + } } - } - } - } + }, + ); + }); }); }); }); From 4940d1c96b991060667ad264362bc3b4c033d9c8 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Sat, 18 Oct 2025 15:47:04 -0700 Subject: [PATCH 10/13] Entity Name References as values. Add PatchContext and TemplateContext --- crates/bevy_asset/src/handle.rs | 7 +- crates/bevy_ecs/macros/src/template.rs | 24 +-- crates/bevy_ecs/src/entity/entity_path.rs | 2 +- crates/bevy_ecs/src/template.rs | 171 ++++++++++++++++--- crates/bevy_scene2/macros/src/bsn/codegen.rs | 107 ++++++++++-- crates/bevy_scene2/macros/src/bsn/mod.rs | 11 +- crates/bevy_scene2/macros/src/bsn/parse.rs | 3 + crates/bevy_scene2/macros/src/bsn/types.rs | 1 + crates/bevy_scene2/src/lib.rs | 17 +- crates/bevy_scene2/src/resolved_scene.rs | 53 +++++- crates/bevy_scene2/src/scene.rs | 118 +++++++++---- crates/bevy_scene2/src/scene_list.rs | 42 ++--- crates/bevy_scene2/src/scene_patch.rs | 6 +- crates/bevy_scene2/src/spawn.rs | 102 ++++++++--- 14 files changed, 503 insertions(+), 161 deletions(-) diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index 4a3f6f8764e74..b4c8d1a477f0f 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -5,8 +5,7 @@ use crate::{ use alloc::sync::Arc; use bevy_ecs::{ error::Result, - template::{GetTemplate, Template}, - world::EntityWorldMut, + template::{GetTemplate, Template, TemplateContext}, }; use bevy_reflect::{Reflect, TypePath}; use core::{ @@ -226,8 +225,8 @@ impl>, T> From for HandleTemplate { impl Template for HandleTemplate { type Output = Handle; - fn build(&mut self, entity: &mut EntityWorldMut) -> Result> { - Ok(entity.resource::().load(&self.path)) + fn build(&mut self, context: &mut TemplateContext) -> Result> { + Ok(context.resource::().load(&self.path)) } } diff --git a/crates/bevy_ecs/macros/src/template.rs b/crates/bevy_ecs/macros/src/template.rs index 2e451d58060a2..60ef4a1e323ac 100644 --- a/crates/bevy_ecs/macros/src/template.rs +++ b/crates/bevy_ecs/macros/src/template.rs @@ -37,7 +37,7 @@ pub(crate) fn derive_get_template(input: TokenStream) -> TokenStream { 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 { + fn build(&mut self, context: &mut #bevy_ecs::template::TemplateContext) -> #bevy_ecs::error::Result { Ok(#type_ident { #(#template_field_builds,)* }) @@ -62,7 +62,7 @@ pub(crate) fn derive_get_template(input: TokenStream) -> TokenStream { 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 { + fn build(&mut self, context: &mut #bevy_ecs::template::TemplateContext) -> #bevy_ecs::error::Result { Ok(#type_ident ( #(#template_field_builds,)* )) @@ -85,7 +85,7 @@ pub(crate) fn derive_get_template(input: TokenStream) -> TokenStream { 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 { + fn build(&mut self, context: &mut #bevy_ecs::template::TemplateContext) -> #bevy_ecs::error::Result { Ok(#type_ident) } } @@ -226,7 +226,7 @@ pub(crate) fn derive_get_template(input: TokenStream) -> TokenStream { 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 { + fn build(&mut self, context: &mut #bevy_ecs::template::TemplateContext) -> #bevy_ecs::error::Result { Ok(match self { #(#variant_builds,)* }) @@ -282,14 +282,14 @@ fn struct_impl(fields: &Fields, bevy_ecs: &Path, is_enum: bool) -> StructImpl { if is_enum { template_field_builds.push(quote! { #ident: match #ident { - #bevy_ecs::template::TemplateField::Template(template) => template.build(entity)?, + #bevy_ecs::template::TemplateField::Template(template) => template.build(context)?, #bevy_ecs::template::TemplateField::Value(value) => Clone::clone(value), } }); } else { template_field_builds.push(quote! { #ident: match &mut self.#ident { - #bevy_ecs::template::TemplateField::Template(template) => template.build(entity)?, + #bevy_ecs::template::TemplateField::Template(template) => template.build(context)?, #bevy_ecs::template::TemplateField::Value(value) => Clone::clone(value), } }); @@ -303,11 +303,11 @@ fn struct_impl(fields: &Fields, bevy_ecs: &Path, is_enum: bool) -> StructImpl { }); if is_enum { template_field_builds.push(quote! { - #ident: #ident.build(entity)? + #ident: #ident.build(context)? }); } else { template_field_builds.push(quote! { - #ident: self.#ident.build(entity)? + #ident: self.#ident.build(context)? }); } @@ -324,14 +324,14 @@ fn struct_impl(fields: &Fields, bevy_ecs: &Path, is_enum: bool) -> StructImpl { 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::Template(template) => template.build(context)?, #bevy_ecs::template::TemplateField::Value(value) => Clone::clone(value), } }); } else { template_field_builds.push(quote! { match &mut self.#index { - #bevy_ecs::template::TemplateField::Template(template) => template.build(entity)?, + #bevy_ecs::template::TemplateField::Template(template) => template.build(context)?, #bevy_ecs::template::TemplateField::Value(value) => Clone::clone(value), } }); @@ -346,11 +346,11 @@ fn struct_impl(fields: &Fields, bevy_ecs: &Path, is_enum: bool) -> StructImpl { if is_enum { let enum_tuple_ident = format_ident!("t{}", index); template_field_builds.push(quote! { - #enum_tuple_ident.build(entity)? + #enum_tuple_ident.build(context)? }); } else { template_field_builds.push(quote! { - self.#index.build(entity)? + self.#index.build(context)? }); } template_field_defaults.push(quote! { diff --git a/crates/bevy_ecs/src/entity/entity_path.rs b/crates/bevy_ecs/src/entity/entity_path.rs index 744de7ca17143..c494c1db41a2f 100644 --- a/crates/bevy_ecs/src/entity/entity_path.rs +++ b/crates/bevy_ecs/src/entity/entity_path.rs @@ -1,6 +1,6 @@ use crate::{entity::Entity, world::EntityWorldMut}; +use alloc::{borrow::Cow, string::String}; use log::warn; -use std::{borrow::Cow, string::String}; use thiserror::Error; /// A path to an entity. diff --git a/crates/bevy_ecs/src/template.rs b/crates/bevy_ecs/src/template.rs index c9f04b63c295f..bb1dc9a203265 100644 --- a/crates/bevy_ecs/src/template.rs +++ b/crates/bevy_ecs/src/template.rs @@ -4,9 +4,10 @@ pub use bevy_ecs_macros::GetTemplate; use crate::{ bundle::Bundle, - entity::{Entity, EntityPath}, + entity::{Entities, Entity, EntityPath}, error::{BevyError, Result}, - world::EntityWorldMut, + resource::Resource, + world::{EntityWorldMut, World}, }; use alloc::{boxed::Box, vec, vec::Vec}; use bevy_platform::collections::hash_map::Entry; @@ -21,13 +22,126 @@ pub trait Template { type Output; /// Uses this template and the given `entity` context to produce a [`Template::Output`]. - fn build(&mut self, entity: &mut EntityWorldMut) -> Result; + fn build(&mut self, context: &mut TemplateContext) -> Result; /// This is used to register information about the template, such as dependencies that should be loaded before it is instantiated. #[inline] fn register_data(&self, _data: &mut TemplateData) {} } +pub struct TemplateContext<'a> { + pub entity: &'a mut EntityWorldMut<'a>, + pub scoped_entities: &'a mut ScopedEntities, + pub entity_scopes: &'a EntityScopes, +} + +impl<'a> TemplateContext<'a> { + pub fn new( + entity: &'a mut EntityWorldMut<'a>, + scoped_entities: &'a mut ScopedEntities, + entity_scopes: &'a EntityScopes, + ) -> Self { + Self { + entity, + scoped_entities, + entity_scopes, + } + } + + pub fn get_scoped_entity(&mut self, scope: usize, index: usize) -> Entity { + self.scoped_entities.get( + // SAFETY: this only uses the world to spawn an empty entity + unsafe { self.entity.world_mut() }, + self.entity_scopes, + scope, + index, + ) + } +} + +#[derive(Default, Debug)] +pub struct EntityScopes { + scopes: Vec>>, + next_index: usize, +} + +impl EntityScopes { + #[inline] + pub fn entity_len(&self) -> usize { + self.next_index + } + pub fn alloc(&mut self, scope: usize, index: usize) { + *self.get_mut(scope, index) = Some(self.next_index); + self.next_index += 1; + } + + pub fn assign(&mut self, scope: usize, index: usize, value: usize) { + let option = self.get_mut(scope, index); + *option = Some(value); + } + + #[allow(unsafe_code)] + fn get_mut(&mut self, scope: usize, index: usize) -> &mut Option { + // NOTE: this is ok because PatchContext::new_scope adds scopes as they are created. + // this shouldn't panic unless internals are broken. + let indices = &mut self.scopes[scope]; + if index >= indices.len() { + indices.resize_with(index + 1, || None); + } + // SAFETY: just allocated above + unsafe { indices.get_unchecked_mut(index) } + } + + pub fn get(&self, scope: usize, index: usize) -> Option { + *self.scopes.get(scope)?.get(index)? + } + + pub fn add_scope(&mut self) -> usize { + let scope_index = self.scopes.len(); + self.scopes.push(Vec::default()); + scope_index + } +} + +#[derive(Debug)] +pub struct ScopedEntities(Vec>); + +impl ScopedEntities { + pub fn new(size: usize) -> Self { + Self(vec![None; size]) + } +} + +impl ScopedEntities { + pub fn get( + &mut self, + world: &mut World, + entity_scopes: &EntityScopes, + scope: usize, + index: usize, + ) -> Entity { + let index = entity_scopes.get(scope, index).unwrap(); + *self.0[index].get_or_insert_with(|| world.spawn_empty().id()) + } + + pub fn set( + &mut self, + entity_scopes: &EntityScopes, + scope: usize, + index: usize, + entity: Entity, + ) { + let index = entity_scopes.get(scope, index).unwrap(); + self.0[index] = Some(entity); + } +} + +impl<'a> TemplateContext<'a> { + pub fn resource(&self) -> &R { + self.entity.resource() + } +} + /// [`GetTemplate`] is implemented for types that can be produced by a specific, canonical [`Template`]. This creates a way to correlate to the [`Template`] using the /// desired template output type. This is used by Bevy's scene system. pub trait GetTemplate: Sized { @@ -43,13 +157,13 @@ macro_rules! template_impl { )] impl<$($template: Template),*> Template for TemplateTuple<($($template,)*)> { type Output = ($($template::Output,)*); - fn build(&mut self, _entity: &mut EntityWorldMut) -> Result { + fn build(&mut self, _context: &mut TemplateContext) -> Result { #[allow( non_snake_case, reason = "The names of these variables are provided by the caller, not by us." )] let ($($template,)*) = &mut self.0; - Ok(($($template.build(_entity)?,)*)) + Ok(($($template.build(_context)?,)*)) } fn register_data(&self, _data: &mut TemplateData) { @@ -70,22 +184,37 @@ pub struct TemplateTuple(pub T); all_tuples!(template_impl, 0, 12, T); -impl Template for EntityPath<'static> { +pub enum EntityReference<'a> { + Path(EntityPath<'a>), + Index { scope: usize, index: usize }, +} + +impl<'a> Default for EntityReference<'a> { + fn default() -> Self { + Self::Path(Default::default()) + } +} + +impl Template for EntityReference<'static> { type Output = Entity; - fn build(&mut self, entity: &mut EntityWorldMut) -> Result { - Ok(entity.resolve_path(self)?) + fn build(&mut self, context: &mut TemplateContext) -> Result { + Ok(match self { + EntityReference::Path(entity_path) => context.entity.resolve_path(entity_path)?, + // unwrap is ok as this is "internals". when implemented correctly this will never panic + EntityReference::Index { scope, index } => context.get_scoped_entity(*scope, *index), + }) } } impl GetTemplate for Entity { - type Template = EntityPath<'static>; + type Template = EntityReference<'static>; } impl Template for T { type Output = T; - fn build(&mut self, _entity: &mut EntityWorldMut) -> Result { + fn build(&mut self, _context: &mut TemplateContext) -> Result { Ok(self.clone()) } } @@ -97,15 +226,15 @@ impl GetTemplate for 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>; + fn apply(&mut self, context: &mut TemplateContext) -> 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); + fn apply(&mut self, context: &mut TemplateContext) -> Result<(), BevyError> { + let bundle = self.build(context)?; + context.entity.insert(bundle); Ok(()) } } @@ -128,9 +257,9 @@ impl Default for TemplateField { impl> Template for TemplateField { type Output = T::Output; - fn build(&mut self, entity: &mut EntityWorldMut) -> Result { + fn build(&mut self, context: &mut TemplateContext) -> Result { Ok(match self { - TemplateField::Template(value) => value.build(entity)?, + TemplateField::Template(value) => value.build(context)?, TemplateField::Value(value) => value.clone(), }) } @@ -142,18 +271,18 @@ 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); +pub struct FnTemplate Result, O>(pub F); -impl Result, O> Template for FnTemplate { +impl Result, O> Template for FnTemplate { type Output = O; - fn build(&mut self, entity: &mut EntityWorldMut) -> Result { - (self.0)(entity) + fn build(&mut self, context: &mut TemplateContext) -> Result { + (self.0)(context) } } /// Returns a "free floating" template for a given `func`. This prevents the need to define a custom type for one-off templates. -pub fn template Result, O>(func: F) -> FnTemplate { +pub fn template Result, O>(func: F) -> FnTemplate { FnTemplate(func) } diff --git a/crates/bevy_scene2/macros/src/bsn/codegen.rs b/crates/bevy_scene2/macros/src/bsn/codegen.rs index 825b8101f686d..9057142bea2a0 100644 --- a/crates/bevy_scene2/macros/src/bsn/codegen.rs +++ b/crates/bevy_scene2/macros/src/bsn/codegen.rs @@ -4,16 +4,50 @@ use crate::bsn::types::{ }; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; +use std::collections::{hash_map::Entry, HashMap}; use syn::{Ident, Index, Lit, Member, Path}; +#[derive(Default)] +pub(crate) struct EntityRefs { + refs: HashMap, + next_index: usize, +} + +impl EntityRefs { + fn get(&mut self, name: String) -> usize { + match self.refs.entry(name) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let index = self.next_index; + entry.insert(index); + self.next_index += 1; + index + } + } + } +} + impl BsnRoot { - pub fn to_tokens(&self, bevy_scene: &Path, bevy_ecs: &Path, bevy_asset: &Path) -> TokenStream { - self.0.to_tokens(bevy_scene, bevy_ecs, bevy_asset) + pub fn to_tokens( + &self, + bevy_scene: &Path, + bevy_ecs: &Path, + bevy_asset: &Path, + entity_refs: &mut EntityRefs, + ) -> TokenStream { + self.0 + .to_tokens(bevy_scene, bevy_ecs, bevy_asset, entity_refs) } } impl Bsn { - pub fn to_tokens(&self, bevy_scene: &Path, bevy_ecs: &Path, bevy_asset: &Path) -> TokenStream { + pub fn to_tokens( + &self, + bevy_scene: &Path, + bevy_ecs: &Path, + bevy_asset: &Path, + entity_refs: &mut EntityRefs, + ) -> TokenStream { let mut entries = Vec::with_capacity(self.entries.len()); for bsn_entry in &self.entries { entries.push(match bsn_entry { @@ -29,10 +63,11 @@ impl Bsn { proc_macro2::Span::call_site(), ))], true, + entity_refs, ); let path = &bsn_type.path; quote! { - <#path as #bevy_scene::PatchTemplate>::patch_template(move |value| { + <#path as #bevy_scene::PatchTemplate>::patch_template(move |value, _context| { #(#assignments)* }) } @@ -49,10 +84,11 @@ impl Bsn { proc_macro2::Span::call_site(), ))], true, + entity_refs, ); let path = &bsn_type.path; quote! { - <#path as #bevy_scene::PatchGetTemplate>::patch(move |value| { + <#path as #bevy_scene::PatchGetTemplate>::patch(move |value, _context| { #(#assignments)* }) } @@ -63,7 +99,7 @@ impl Bsn { } => { quote! { <#type_path as #bevy_scene::PatchTemplate>::patch_template( - move |value| { + move |value, _context| { *value = #type_path::#const_ident; }, ) @@ -79,7 +115,7 @@ impl Bsn { }) => { quote! { <#type_path as #bevy_scene::PatchTemplate>::patch_template( - move |value| { + move |value, _context| { *value = #type_path::#function(#args); }, ) @@ -93,7 +129,7 @@ impl Bsn { // NOTE: The odd turbofish line break below avoids breaking rustfmt quote! { <#type_path as #bevy_scene::PatchGetTemplate>::patch( - move |value| { + move |value, _context| { *value = <#type_path as #bevy_ecs::template::GetTemplate> ::Template::#function(#args); } @@ -101,7 +137,10 @@ impl Bsn { } } BsnEntry::ChildrenSceneList(scene_list) => { - let scenes = scene_list.0.to_tokens(bevy_scene, bevy_ecs, bevy_asset); + let scenes = + scene_list + .0 + .to_tokens(bevy_scene, bevy_ecs, bevy_asset, entity_refs); quote! { #bevy_scene::RelatedScenes::<#bevy_ecs::hierarchy::ChildOf, _>::new(#scenes) } @@ -110,7 +149,10 @@ impl Bsn { scene_list, relationship_path, }) => { - let scenes = scene_list.0.to_tokens(bevy_scene, bevy_ecs, bevy_asset); + let scenes = + scene_list + .0 + .to_tokens(bevy_scene, bevy_ecs, bevy_asset, entity_refs); // NOTE: The odd turbofish line breaks below avoid breaking rustfmt quote! { #bevy_scene::RelatedScenes::< @@ -132,18 +174,18 @@ impl Bsn { }, BsnEntry::Name(ident) => { let name = ident.to_string(); + let index = entity_refs.get(name.clone()); quote! { - <#bevy_ecs::name::Name as PatchGetTemplate>::patch( - move |value| { - *value = Name(#name.into()); - } - ) + #bevy_scene::NameEntityReference { + name: Name(#name.into()), + index: #index, + } } } BsnEntry::NameExpression(expr_tokens) => { quote! { <#bevy_ecs::name::Name as PatchGetTemplate>::patch( - move |value| { + move |value, _context| { *value = Name({#expr_tokens}.into()); } ) @@ -175,6 +217,7 @@ impl BsnType { is_root_template: bool, field_path: &[Member], is_path_ref: bool, + entity_refs: &mut EntityRefs, ) { let path = &self.path; if !is_root_template { @@ -202,6 +245,7 @@ impl BsnType { false, &[Member::Named(name.clone())], true, + entity_refs, ); quote! {#(#type_assignments)*} } @@ -236,6 +280,7 @@ impl BsnType { false, &[Member::Named(name.clone())], true, + entity_refs, ); quote! {#(#type_assignments)*} } @@ -275,6 +320,13 @@ impl BsnType { .push(quote! {#(#field_path.)*#field_name = #field_value;}); } } + Some(BsnValue::Name(ident)) => { + let name = ident.to_string(); + let index = entity_refs.get(name); + assignments.push(quote!{ + #(#field_path.)*#field_name = #bevy_ecs::template::EntityReference::Index { scope: _context.current_scope(), index: #index }; + }) + } Some(BsnValue::Type(field_type)) => { if field_type.enum_variant.is_some() { assignments @@ -289,6 +341,7 @@ impl BsnType { false, &new_field_path, false, + entity_refs, ); } } @@ -311,6 +364,13 @@ impl BsnType { ); } } + BsnValue::Name(ident) => { + let name = ident.to_string(); + let index = entity_refs.get(name); + assignments.push(quote!{ + #(#field_path.)*#field_index = #bevy_ecs::template::EntityReference::Index { scope: _context.current_scope(), index: #index }; + }) + } BsnValue::Type(field_type) => { if field_type.enum_variant.is_some() { assignments @@ -325,6 +385,7 @@ impl BsnType { false, &new_field_path, false, + entity_refs, ); } } @@ -337,10 +398,16 @@ impl BsnType { } impl BsnSceneListItems { - pub fn to_tokens(&self, bevy_scene: &Path, bevy_ecs: &Path, bevy_asset: &Path) -> TokenStream { + pub fn to_tokens( + &self, + bevy_scene: &Path, + bevy_ecs: &Path, + bevy_asset: &Path, + entity_refs: &mut EntityRefs, + ) -> TokenStream { let scenes = self.0.iter().map(|scene| match scene { BsnSceneListItem::Scene(bsn) => { - let tokens = bsn.to_tokens(bevy_scene, bevy_ecs, bevy_asset); + let tokens = bsn.to_tokens(bevy_scene, bevy_ecs, bevy_asset, entity_refs); quote! {#bevy_scene::EntityScene(#tokens)} } BsnSceneListItem::Expression(block) => quote! {#block}, @@ -408,6 +475,10 @@ impl ToTokens for BsnValue { BsnValue::Type(ty) => { ty.to_tokens(tokens); } + BsnValue::Name(ident) => { + // Name requires additional context to convert to tokens + unreachable!() + } }; } } diff --git a/crates/bevy_scene2/macros/src/bsn/mod.rs b/crates/bevy_scene2/macros/src/bsn/mod.rs index 3e308604a64a0..3d65533ac84d7 100644 --- a/crates/bevy_scene2/macros/src/bsn/mod.rs +++ b/crates/bevy_scene2/macros/src/bsn/mod.rs @@ -1,4 +1,7 @@ -use crate::bsn::types::{BsnRoot, BsnSceneListItems}; +use crate::bsn::{ + codegen::EntityRefs, + types::{BsnRoot, BsnSceneListItems}, +}; use bevy_macro_utils::BevyManifest; use proc_macro::TokenStream; use syn::parse_macro_input; @@ -13,7 +16,8 @@ pub fn bsn(input: TokenStream) -> TokenStream { 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)) + let mut entity_refs = EntityRefs::default(); + TokenStream::from(scene.to_tokens(&bevy_scene, &bevy_ecs, &bevy_asset, &mut entity_refs)) } pub fn bsn_list(input: TokenStream) -> TokenStream { @@ -22,5 +26,6 @@ pub fn bsn_list(input: TokenStream) -> TokenStream { 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)) + let mut entity_refs = EntityRefs::default(); + TokenStream::from(scene.to_tokens(&bevy_scene, &bevy_ecs, &bevy_asset, &mut entity_refs)) } diff --git a/crates/bevy_scene2/macros/src/bsn/parse.rs b/crates/bevy_scene2/macros/src/bsn/parse.rs index f0eafb15fbb1a..94854fde44ddd 100644 --- a/crates/bevy_scene2/macros/src/bsn/parse.rs +++ b/crates/bevy_scene2/macros/src/bsn/parse.rs @@ -380,6 +380,9 @@ impl Parse for BsnValue { BsnValue::Lit(input.parse::()?) } else if input.peek(Paren) { BsnValue::Tuple(input.parse::()?) + } else if input.peek(Token![#]) { + input.parse::()?; + BsnValue::Name(input.parse::()?) } else { return Err(input.error( "BsnValue parse for this input is not supported yet, nor is proper error handling :)" diff --git a/crates/bevy_scene2/macros/src/bsn/types.rs b/crates/bevy_scene2/macros/src/bsn/types.rs index 6a64b3ad129a9..938e282d4037e 100644 --- a/crates/bevy_scene2/macros/src/bsn/types.rs +++ b/crates/bevy_scene2/macros/src/bsn/types.rs @@ -97,4 +97,5 @@ pub enum BsnValue { Lit(Lit), Type(BsnType), Tuple(BsnTuple), + Name(Ident), } diff --git a/crates/bevy_scene2/src/lib.rs b/crates/bevy_scene2/src/lib.rs index 318b805582006..11e623c9bf3cc 100644 --- a/crates/bevy_scene2/src/lib.rs +++ b/crates/bevy_scene2/src/lib.rs @@ -23,7 +23,11 @@ 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 bevy_ecs::{ + prelude::*, + system::IntoObserverSystem, + template::{Template, TemplateContext}, +}; use std::marker::PhantomData; #[derive(Default)] @@ -70,8 +74,8 @@ impl + Clone, E: EntityEvent, B: Bundle, M: 'stat { type Output = (); - fn build(&mut self, entity: &mut EntityWorldMut) -> Result { - entity.observe(self.0.clone()); + fn build(&mut self, context: &mut TemplateContext) -> Result { + context.entity.observe(self.0.clone()); Ok(()) } } @@ -83,12 +87,7 @@ impl< M: 'static, > Scene for OnTemplate { - fn patch( - &self, - _assets: &AssetServer, - _patches: &bevy_asset::Assets, - scene: &mut ResolvedScene, - ) { + fn patch(&self, _context: &mut PatchContext, scene: &mut ResolvedScene) { scene.push_template(OnTemplate(self.0.clone(), PhantomData)); } } diff --git a/crates/bevy_scene2/src/resolved_scene.rs b/crates/bevy_scene2/src/resolved_scene.rs index 01f68b03265fb..a882e5c67c471 100644 --- a/crates/bevy_scene2/src/resolved_scene.rs +++ b/crates/bevy_scene2/src/resolved_scene.rs @@ -3,7 +3,7 @@ use bevy_ecs::{ entity::Entity, error::Result, relationship::Relationship, - template::{ErasedTemplate, Template}, + template::{ErasedTemplate, Template, TemplateContext}, world::EntityWorldMut, }; use bevy_utils::TypeIdMap; @@ -15,22 +15,53 @@ pub struct ResolvedScene { pub templates: Vec>, // PERF: special casing children probably makes sense here pub related: TypeIdMap, + pub entity_references: Vec<(usize, usize)>, +} + +impl std::fmt::Debug for ResolvedScene { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResolvedScene") + .field("related", &self.related) + .field("entity_references", &self.entity_references) + .finish() + } } impl ResolvedScene { - pub fn spawn(&mut self, entity: &mut EntityWorldMut) -> Result { + pub fn apply(&mut self, context: &mut TemplateContext) -> Result { + if let Some((scope, index)) = self.entity_references.first().copied() { + context + .scoped_entities + .set(context.entity_scopes, scope, index, context.entity.id()); + } for template in self.templates.iter_mut() { - template.apply(entity)?; + template.apply(context)?; } for related in self.related.values_mut() { - let target = entity.id(); - entity.world_scope(|world| -> Result { + let target = context.entity.id(); + context.entity.world_scope(|world| -> Result { + // TODO: I think we need to scan the scene and resolve entities ahead of time, in order to dedupe? Or is there a way to do that + // at patch time? for scene in &mut related.scenes { - let mut entity = world.spawn_empty(); + let mut entity = if let Some((scope, index)) = + scene.entity_references.first().copied() + { + let entity = + context + .scoped_entities + .get(world, context.entity_scopes, scope, index); + world.entity_mut(entity) + } else { + world.spawn_empty() + }; (related.insert)(&mut entity, target); // PERF: this will result in an archetype move - scene.spawn(&mut entity)?; + scene.apply(&mut TemplateContext::new( + &mut entity, + context.scoped_entities, + context.entity_scopes, + ))?; } Ok(()) })?; @@ -66,6 +97,14 @@ pub struct ResolvedRelatedScenes { pub insert: fn(&mut EntityWorldMut, target: Entity), } +impl std::fmt::Debug for ResolvedRelatedScenes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResolvedRelatedScenes") + .field("scenes", &self.scenes) + .finish() + } +} + impl ResolvedRelatedScenes { pub fn new() -> Self { Self { diff --git a/crates/bevy_scene2/src/scene.rs b/crates/bevy_scene2/src/scene.rs index 75c77d7f0f8b4..b90df67240c64 100644 --- a/crates/bevy_scene2/src/scene.rs +++ b/crates/bevy_scene2/src/scene.rs @@ -3,28 +3,53 @@ use bevy_asset::{AssetPath, AssetServer, Assets}; use bevy_ecs::{ bundle::Bundle, error::Result, + name::Name, relationship::Relationship, - template::{FnTemplate, GetTemplate, Template}, - world::EntityWorldMut, + template::{EntityScopes, FnTemplate, GetTemplate, Template, TemplateContext}, }; use std::{any::TypeId, marker::PhantomData}; use variadics_please::all_tuples; pub trait Scene: Send + Sync + 'static { - fn patch(&self, assets: &AssetServer, patches: &Assets, scene: &mut ResolvedScene); + fn patch(&self, context: &mut PatchContext, scene: &mut ResolvedScene); fn register_dependencies(&self, _dependencies: &mut Vec>) {} } +pub struct PatchContext<'a> { + pub assets: &'a AssetServer, + pub patches: &'a Assets, + pub(crate) entity_scopes: &'a mut EntityScopes, + pub(crate) current_scope: usize, +} + +impl<'a> PatchContext<'a> { + #[inline] + pub fn current_scope(&self) -> usize { + self.current_scope + } + + pub fn new_scope(&mut self, func: impl FnOnce(&mut PatchContext)) { + let current_scope = self.entity_scopes.add_scope(); + let mut context = PatchContext { + assets: self.assets, + patches: self.patches, + entity_scopes: self.entity_scopes, + current_scope, + }; + (func)(&mut context); + } +} + macro_rules! scene_impl { ($($patch: ident),*) => { impl<$($patch: Scene),*> Scene for ($($patch,)*) { - fn patch(&self, _assets: &AssetServer, _patches: &Assets, _scene: &mut ResolvedScene) { + fn patch(&self, _context: &mut PatchContext, _scene: &mut ResolvedScene) { #[allow( non_snake_case, reason = "The names of these variables are provided by the caller, not by us." )] let ($($patch,)*) = self; - $($patch.patch(_assets, _patches, _scene);)* + $($patch.patch(_context, _scene);)* } fn register_dependencies(&self, _dependencies: &mut Vec>) { @@ -41,13 +66,13 @@ macro_rules! scene_impl { all_tuples!(scene_impl, 0, 12, P); -pub struct TemplatePatch(pub F, pub PhantomData); +pub struct TemplatePatch(pub F, pub PhantomData); pub fn template_value( value: T, -) -> TemplatePatch { +) -> TemplatePatch { TemplatePatch( - move |input: &mut T| { + move |input: &mut T, _context: &mut PatchContext| { *input = value.clone(); }, PhantomData, @@ -56,39 +81,38 @@ pub fn template_value( pub trait PatchGetTemplate { type Template; - fn patch(func: F) -> TemplatePatch; + fn patch( + func: F, + ) -> TemplatePatch; } impl PatchGetTemplate for G { type Template = G::Template; - fn patch(func: F) -> TemplatePatch { + fn patch( + func: F, + ) -> TemplatePatch { TemplatePatch(func, PhantomData) } } pub trait PatchTemplate: Sized { - fn patch_template(func: F) -> TemplatePatch; + fn patch_template(func: F) -> TemplatePatch; } impl PatchTemplate for T { - fn patch_template(func: F) -> TemplatePatch { + fn patch_template(func: F) -> TemplatePatch { TemplatePatch(func, PhantomData) } } impl< - F: Fn(&mut T) + Send + Sync + 'static, + F: Fn(&mut T, &mut PatchContext) + Send + Sync + 'static, T: Template + Send + Sync + Default + 'static, > Scene for TemplatePatch { - fn patch( - &self, - _assets: &AssetServer, - _patches: &Assets, - scene: &mut ResolvedScene, - ) { + fn patch(&self, context: &mut PatchContext, scene: &mut ResolvedScene) { let template = scene.get_or_insert_template::(); - (self.0)(template); + (self.0)(template, context); } } @@ -107,13 +131,13 @@ impl RelatedScenes { } impl Scene for RelatedScenes { - fn patch(&self, assets: &AssetServer, patches: &Assets, scene: &mut ResolvedScene) { + fn patch(&self, context: &mut PatchContext, scene: &mut ResolvedScene) { let related = scene .related .entry(TypeId::of::()) .or_insert_with(ResolvedRelatedScenes::new::); self.related_template_list - .patch_list(assets, patches, &mut related.scenes); + .patch_list(context, &mut related.scenes); } fn register_dependencies(&self, dependencies: &mut Vec>) { @@ -125,8 +149,10 @@ impl Scene for RelatedScenes { 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 patch(&self, context: &mut PatchContext, scene: &mut ResolvedScene) { + context.new_scope(|context| { + self.0.patch(context, scene); + }); } fn register_dependencies(&self, dependencies: &mut Vec>) { @@ -144,10 +170,12 @@ impl>> From for InheritSceneAsset { } 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 patch(&self, context: &mut PatchContext, scene: &mut ResolvedScene) { + let id = context.assets.get_path_id(&self.0).unwrap(); + let scene_patch = context.patches.get(id.typed()).unwrap(); + context.new_scope(|context| { + scene_patch.patch.patch(context, scene); + }); } fn register_dependencies(&self, dependencies: &mut Vec>) { @@ -155,15 +183,35 @@ impl Scene for InheritSceneAsset { } } -impl Result) + Clone + Send + Sync + 'static, O: Bundle> Scene +impl Result) + Clone + Send + Sync + 'static, O: Bundle> Scene for FnTemplate { - fn patch( - &self, - _assets: &AssetServer, - _patches: &Assets, - scene: &mut ResolvedScene, - ) { + fn patch(&self, _context: &mut PatchContext, scene: &mut ResolvedScene) { scene.push_template(FnTemplate(self.0.clone())); } } + +pub struct NameEntityReference { + pub name: Name, + pub index: usize, +} + +impl Scene for NameEntityReference { + fn patch(&self, context: &mut PatchContext, scene: &mut ResolvedScene) { + if let Some((scope, index)) = scene.entity_references.first().copied() { + let entity_index = context.entity_scopes.get(scope, index).unwrap(); + context + .entity_scopes + .assign(context.current_scope, self.index, entity_index); + } else { + context + .entity_scopes + .alloc(context.current_scope, self.index); + } + scene + .entity_references + .push((context.current_scope, self.index)); + let name = scene.get_or_insert_template::(); + *name = self.name.clone(); + } +} diff --git a/crates/bevy_scene2/src/scene_list.rs b/crates/bevy_scene2/src/scene_list.rs index 8047767f52fe1..0c4329ef8b56c 100644 --- a/crates/bevy_scene2/src/scene_list.rs +++ b/crates/bevy_scene2/src/scene_list.rs @@ -1,14 +1,9 @@ -use crate::{ResolvedScene, Scene, ScenePatch}; -use bevy_asset::{AssetPath, AssetServer, Assets}; +use crate::{PatchContext, ResolvedScene, Scene}; +use bevy_asset::AssetPath; use variadics_please::all_tuples; pub trait SceneList: Send + Sync + 'static { - fn patch_list( - &self, - assets: &AssetServer, - patches: &Assets, - scenes: &mut Vec, - ); + fn patch_list(&self, context: &mut PatchContext, scenes: &mut Vec); fn register_dependencies(&self, dependencies: &mut Vec>); } @@ -16,14 +11,9 @@ pub trait SceneList: Send + Sync + 'static { pub struct EntityScene(pub S); impl SceneList for EntityScene { - fn patch_list( - &self, - assets: &AssetServer, - patches: &Assets, - scenes: &mut Vec, - ) { + fn patch_list(&self, context: &mut PatchContext, scenes: &mut Vec) { let mut resolved_scene = ResolvedScene::default(); - self.0.patch(assets, patches, &mut resolved_scene); + self.0.patch(context, &mut resolved_scene); scenes.push(resolved_scene); } @@ -35,13 +25,13 @@ impl SceneList for EntityScene { macro_rules! scene_list_impl { ($($list: ident),*) => { impl<$($list: SceneList),*> SceneList for ($($list,)*) { - fn patch_list(&self, _assets: &AssetServer, _patches: &Assets, _scenes: &mut Vec) { + fn patch_list(&self, _context: &mut PatchContext, _scenes: &mut Vec) { #[allow( non_snake_case, reason = "The names of these variables are provided by the caller, not by us." )] let ($($list,)*) = self; - $($list.patch_list(_assets, _patches, _scenes);)* + $($list.patch_list(_context, _scenes);)* } fn register_dependencies(&self, _dependencies: &mut Vec>) { @@ -59,15 +49,10 @@ macro_rules! scene_list_impl { all_tuples!(scene_list_impl, 0, 12, P); impl SceneList for Vec { - fn patch_list( - &self, - assets: &AssetServer, - patches: &Assets, - scenes: &mut Vec, - ) { + fn patch_list(&self, context: &mut PatchContext, scenes: &mut Vec) { for scene in self { let mut resolved_scene = ResolvedScene::default(); - scene.patch(assets, patches, &mut resolved_scene); + scene.patch(context, &mut resolved_scene); scenes.push(resolved_scene); } } @@ -80,15 +65,10 @@ impl SceneList for Vec { } impl SceneList for Vec> { - fn patch_list( - &self, - assets: &AssetServer, - patches: &Assets, - scenes: &mut Vec, - ) { + fn patch_list(&self, context: &mut PatchContext, scenes: &mut Vec) { for scene in self { let mut resolved_scene = ResolvedScene::default(); - scene.patch(assets, patches, &mut resolved_scene); + scene.patch(context, &mut resolved_scene); scenes.push(resolved_scene); } } diff --git a/crates/bevy_scene2/src/scene_patch.rs b/crates/bevy_scene2/src/scene_patch.rs index d261b969262b7..de4e3d7ac4065 100644 --- a/crates/bevy_scene2/src/scene_patch.rs +++ b/crates/bevy_scene2/src/scene_patch.rs @@ -1,7 +1,7 @@ use crate::{ResolvedScene, Scene, SceneList}; use bevy_asset::{Asset, AssetServer, Handle, UntypedHandle}; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::component::Component; +use bevy_ecs::{component::Component, template::EntityScopes}; use bevy_reflect::TypePath; #[derive(Asset, TypePath)] @@ -11,6 +11,7 @@ pub struct ScenePatch { pub dependencies: Vec, // TODO: consider breaking this out to prevent mutating asset events when resolved pub resolved: Option, + pub entity_scopes: Option, } impl ScenePatch { @@ -25,6 +26,7 @@ impl ScenePatch { patch: Box::new(scene), dependencies, resolved: None, + entity_scopes: None, } } } @@ -39,6 +41,7 @@ pub struct SceneListPatch { pub dependencies: Vec, // TODO: consider breaking this out to prevent mutating asset events when resolved pub resolved: Option>, + pub entity_scopes: Option, } impl SceneListPatch { @@ -53,6 +56,7 @@ impl SceneListPatch { patch: Box::new(scene_list), dependencies, resolved: None, + entity_scopes: None, } } } diff --git a/crates/bevy_scene2/src/spawn.rs b/crates/bevy_scene2/src/spawn.rs index 134757eb3102f..958960ed55601 100644 --- a/crates/bevy_scene2/src/spawn.rs +++ b/crates/bevy_scene2/src/spawn.rs @@ -1,6 +1,13 @@ -use crate::{ResolvedScene, Scene, SceneList, SceneListPatch, ScenePatch, ScenePatchInstance}; +use crate::{ + PatchContext, ResolvedScene, Scene, SceneList, SceneListPatch, ScenePatch, ScenePatchInstance, +}; use bevy_asset::{AssetEvent, AssetId, AssetServer, Assets, Handle}; -use bevy_ecs::{message::MessageCursor, prelude::*, relationship::Relationship}; +use bevy_ecs::{ + message::MessageCursor, + prelude::*, + relationship::Relationship, + template::{EntityScopes, ScopedEntities, TemplateContext}, +}; use bevy_platform::collections::HashMap; pub trait SpawnScene { @@ -83,11 +90,21 @@ pub fn resolve_scene_patches( // TODO: handle modified? AssetEvent::LoadedWithDependencies { id } => { let mut scene = ResolvedScene::default(); + let mut entity_scopes = EntityScopes::default(); // TODO: real error handling let patch = patches.get(id).unwrap(); - patch.patch.patch(&assets, &patches, &mut scene); + patch.patch.patch( + &mut PatchContext { + assets: &assets, + patches: &patches, + current_scope: entity_scopes.add_scope(), + entity_scopes: &mut entity_scopes, + }, + &mut scene, + ); let patch = patches.get_mut(id).unwrap(); - patch.resolved = Some(scene) + patch.resolved = Some(scene); + patch.entity_scopes = Some(entity_scopes); } _ => {} } @@ -97,11 +114,20 @@ pub fn resolve_scene_patches( // TODO: handle modified? AssetEvent::LoadedWithDependencies { id } => { let list_patch = list_patches.get_mut(id).unwrap(); + let mut entity_scopes = EntityScopes::default(); let mut scenes = Vec::new(); // TODO: real error handling - list_patch.patch.patch_list(&assets, &patches, &mut scenes); - println!("LOADED {}", scenes.len()); + list_patch.patch.patch_list( + &mut PatchContext { + assets: &assets, + patches: &patches, + current_scope: entity_scopes.add_scope(), + entity_scopes: &mut entity_scopes, + }, + &mut scenes, + ); list_patch.resolved = Some(scenes); + list_patch.entity_scopes = Some(entity_scopes); } _ => {} } @@ -151,12 +177,22 @@ pub fn spawn_queued( } 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()) + if let Some((Some(scene), Some(entity_scopes))) = + patches.get_mut(id).map(|p| { + (p.resolved.as_mut(), p.entity_scopes.as_ref()) + }) { let mut entity_mut = world.get_entity_mut(entity).unwrap(); - scene.spawn(&mut entity_mut).unwrap(); + scene + .apply(&mut TemplateContext::new( + &mut entity_mut, + &mut ScopedEntities::new( + entity_scopes.entity_len(), + ), + entity_scopes, + )) + .unwrap(); } else { let entities = queued.waiting_entities.entry(id).or_default(); @@ -173,9 +209,10 @@ pub fn spawn_queued( for (scene_list_spawn, handle) in core::mem::take(&mut new_scenes.scene_entities) { - if let Some(resolved_scenes) = list_patches - .get_mut(&handle) - .and_then(|p| p.resolved.as_mut()) + if let Some((Some(resolved_scenes), Some(entity_scopes))) = + list_patches.get_mut(&handle).map(|p| { + (p.resolved.as_mut(), p.entity_scopes.as_ref()) + }) { for scene in resolved_scenes { let mut child_entity = world.spawn_empty(); @@ -183,7 +220,15 @@ pub fn spawn_queued( &mut child_entity, scene_list_spawn.entity, ); - scene.spawn(&mut child_entity).unwrap(); + scene + .apply(&mut TemplateContext::new( + &mut child_entity, + &mut ScopedEntities::new( + entity_scopes.entity_len(), + ), + entity_scopes, + )) + .unwrap(); } } else { let entities = queued @@ -197,21 +242,32 @@ pub fn spawn_queued( 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((Some(scene), Some(entity_scopes))) = patches + .get_mut(*id) + .map(|p| (p.resolved.as_mut(), p.entity_scopes.as_ref())) && let Some(entities) = queued.waiting_entities.remove(id) { for entity in entities { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { - scene.spawn(&mut entity_mut).unwrap(); + scene + .apply(&mut TemplateContext::new( + &mut entity_mut, + &mut ScopedEntities::new( + entity_scopes.entity_len(), + ), + entity_scopes, + )) + .unwrap(); } } } } for event in list_reader.read(&list_events) { if let AssetEvent::LoadedWithDependencies { id } = event - && let Some(resolved_scenes) = - list_patches.get_mut(*id).and_then(|p| p.resolved.as_mut()) + && let Some((Some(resolved_scenes), Some(entity_scopes))) = + list_patches.get_mut(*id).map(|p| { + (p.resolved.as_mut(), p.entity_scopes.as_ref()) + }) && let Some(scene_list_spawns) = queued.waiting_list_entities.remove(id) { @@ -222,7 +278,15 @@ pub fn spawn_queued( &mut child_entity, scene_list_spawn.entity, ); - scene.spawn(&mut child_entity).unwrap(); + scene + .apply(&mut TemplateContext::new( + &mut child_entity, + &mut ScopedEntities::new( + entity_scopes.entity_len(), + ), + entity_scopes, + )) + .unwrap(); } } } From a9b985bcbd9b9a4bbfd1f6f1d38d86f6895a0bed Mon Sep 17 00:00:00 2001 From: Talin Date: Mon, 20 Oct 2025 13:41:51 -0700 Subject: [PATCH 11/13] Porting feathers menu to latest bsn branch. (#47) * Porting feathers menu to latest bsn branch. * Remove unnecessary template_value --------- Co-authored-by: Carter Anderson --- .../bevy_feathers/src/assets/icons/_NAMING.md | 2 + .../src/assets/icons/chevron-down.png | Bin 0 -> 209 bytes .../src/assets/icons/chevron-right.png | Bin 0 -> 206 bytes crates/bevy_feathers/src/assets/icons/x.png | Bin 0 -> 255 bytes crates/bevy_feathers/src/constants.rs | 10 + crates/bevy_feathers/src/controls/menu.rs | 329 +++++++++++++++ crates/bevy_feathers/src/controls/mod.rs | 3 + crates/bevy_feathers/src/dark_theme.rs | 10 + crates/bevy_feathers/src/icon.rs | 18 + crates/bevy_feathers/src/lib.rs | 10 + crates/bevy_feathers/src/tokens.rs | 16 + crates/bevy_input_focus/src/tab_navigation.rs | 45 ++- crates/bevy_math/src/rects/rect.rs | 32 ++ crates/bevy_ui/src/ui_node.rs | 2 +- crates/bevy_ui_widgets/Cargo.toml | 1 + crates/bevy_ui_widgets/src/lib.rs | 7 + crates/bevy_ui_widgets/src/menu.rs | 377 ++++++++++++++++++ crates/bevy_ui_widgets/src/popover.rs | 246 ++++++++++++ examples/ui/feathers.rs | 41 +- 19 files changed, 1139 insertions(+), 10 deletions(-) create mode 100644 crates/bevy_feathers/src/assets/icons/_NAMING.md create mode 100644 crates/bevy_feathers/src/assets/icons/chevron-down.png create mode 100644 crates/bevy_feathers/src/assets/icons/chevron-right.png create mode 100644 crates/bevy_feathers/src/assets/icons/x.png create mode 100644 crates/bevy_feathers/src/controls/menu.rs create mode 100644 crates/bevy_feathers/src/icon.rs create mode 100644 crates/bevy_ui_widgets/src/menu.rs create mode 100644 crates/bevy_ui_widgets/src/popover.rs diff --git a/crates/bevy_feathers/src/assets/icons/_NAMING.md b/crates/bevy_feathers/src/assets/icons/_NAMING.md new file mode 100644 index 0000000000000..baffdedb4103b --- /dev/null +++ b/crates/bevy_feathers/src/assets/icons/_NAMING.md @@ -0,0 +1,2 @@ +The names of files in this directory should refer to what the icons look like ("x", "chevron", etc.) +rather than their assigned meanings ("close", "expand") because the latter can change. diff --git a/crates/bevy_feathers/src/assets/icons/chevron-down.png b/crates/bevy_feathers/src/assets/icons/chevron-down.png new file mode 100644 index 0000000000000000000000000000000000000000..ba8a4c42104e05da6413dda53f658614d34becf7 GIT binary patch literal 209 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|x;gt_8Y&u=pv;%nC}`o4Av!;1HB zk{S-5dG_0qQD;B1>V>P;+PV`!^59lTaPgg&ebxsLQ E0DnGF00000 literal 0 HcmV?d00001 diff --git a/crates/bevy_feathers/src/assets/icons/x.png b/crates/bevy_feathers/src/assets/icons/x.png new file mode 100644 index 0000000000000000000000000000000000000000..23cff4ac589c8901796eea1e7906c7a8b25ed058 GIT binary patch literal 255 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|c6z!vhIn|t z4LK;-?7$H-%R}~O2KUd1b@R7Os0nKKeefXRnB~(4Za*vE?~RQR<2czr>1QGL4nFy) zBWL9QS_)hYNltvm-VkWua%K9BSq|(ExDSZjoS=V0tZ}PW@Di2nT!z6R${myRx3T{H zTfV5`dTf26p*x+|(sbFx zIqn9}CVUm!X=z`qRn$MXx>=fGp= literal 0 HcmV?d00001 diff --git a/crates/bevy_feathers/src/constants.rs b/crates/bevy_feathers/src/constants.rs index 359e5a4935b0c..14789f0083c39 100644 --- a/crates/bevy_feathers/src/constants.rs +++ b/crates/bevy_feathers/src/constants.rs @@ -14,6 +14,16 @@ pub mod fonts { pub const MONO: &str = "embedded://bevy_feathers/assets/fonts/FiraMono-Medium.ttf"; } +/// Icon paths +pub mod icons { + /// Downward-pointing chevron + pub const CHEVRON_DOWN: &str = "embedded://bevy_feathers/assets/icons/chevron-down.png"; + /// Right-pointing chevron + pub const CHEVRON_RIGHT: &str = "embedded://bevy_feathers/assets/icons/chevron-right.png"; + /// Diagonal Cross + pub const X: &str = "embedded://bevy_feathers/assets/icons/x.png"; +} + /// Size constants pub mod size { use bevy_ui::Val; diff --git a/crates/bevy_feathers/src/controls/menu.rs b/crates/bevy_feathers/src/controls/menu.rs new file mode 100644 index 0000000000000..cc30c2623835a --- /dev/null +++ b/crates/bevy_feathers/src/controls/menu.rs @@ -0,0 +1,329 @@ +use alloc::sync::Arc; + +use bevy_app::{Plugin, PreUpdate}; +use bevy_camera::visibility::Visibility; +use bevy_color::{Alpha, Srgba}; +use bevy_ecs::{ + component::Component, + entity::Entity, + hierarchy::Children, + lifecycle::RemovedComponents, + observer::On, + query::{Added, Changed, Has, Or, With}, + schedule::IntoScheduleConfigs, + system::{Commands, EntityCommands, Query}, +}; +use bevy_log::info; +use bevy_picking::{ + events::{Click, Pointer}, + hover::Hovered, + PickingSystems, +}; +use bevy_scene2::{prelude::*, template_value}; +use bevy_ui::{ + AlignItems, BoxShadow, Display, FlexDirection, GlobalZIndex, InteractionDisabled, + JustifyContent, Node, OverrideClip, PositionType, Pressed, UiRect, Val, +}; +use bevy_ui_widgets::{ + popover::{Popover, PopoverAlign, PopoverPlacement, PopoverSide}, + MenuAction, MenuEvent, MenuItem, MenuPopup, +}; + +use crate::{ + constants::{fonts, icons, size}, + controls::{button, ButtonProps, ButtonVariant}, + font_styles::InheritableFont, + icon, + rounded_corners::RoundedCorners, + theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, + tokens, +}; +use bevy_input_focus::tab_navigation::TabIndex; + +/// Parameters for the menu button template, passed to [`menu_button`] function. +#[derive(Default)] +pub struct MenuButtonProps { + /// Rounded corners options + pub corners: RoundedCorners, +} + +/// Marker for menu items +#[derive(Component, Default, Clone)] +struct MenuItemStyle; + +/// Marker for menu popup +#[derive(Component, Default, Clone)] +struct MenuPopupStyle; + +/// Marker for menu wrapper +#[derive(Component, Clone, Default)] +struct Menu(Option>); + +/// Menu scene function. This wraps the menu button and provides an anchor for the popopver. +pub fn menu(spawn_popover: F) -> impl Scene { + let menu = Menu(Some(Arc::new(spawn_popover))); + bsn! { + Node { + height: size::ROW_HEIGHT, + justify_content: JustifyContent::Stretch, + align_items: AlignItems::Stretch, + } + template_value(menu) + on(| + ev: On, + q_menu: Query<(&Menu, &Children)>, + q_popovers: Query>, + // mut redraw_events: MessageWriter, + mut commands: Commands| { + match ev.event().action { + // MenuEvent::Open => todo!(), + // MenuEvent::Close => todo!(), + MenuAction::Toggle => { + let mut was_open = false; + let Ok((menu, children)) = q_menu.get(ev.source) else { + return; + }; + for child in children.iter() { + if q_popovers.contains(*child) { + commands.entity(*child).despawn(); + was_open = true; + } + } + // Spawn the menu if not already open. + if !was_open { + info!("Opening, !was_open"); + if let Some(factory) = menu.0.as_ref() { + (*factory)(commands.entity(ev.source)); + // redraw_events.write(RequestRedraw); + } + } + }, + MenuAction::CloseAll => { + let Ok((_menu, children)) = q_menu.get(ev.source) else { + return; + }; + for child in children.iter() { + if q_popovers.contains(*child) { + commands.entity(*child).despawn(); + } + } + }, + // MenuEvent::FocusRoot => todo!(), + event => { + info!("Menu Event: {:?}", event); + } + } + }) + } +} + +/// Button scene function. +/// +/// # Arguments +/// * `props` - construction properties for the button. +pub fn menu_button(props: MenuButtonProps) -> impl Scene { + bsn! { + :button(ButtonProps { + variant: ButtonVariant::Normal, + corners: props.corners, + }) + Node { + // TODO: HACK to deal with lack of intercepted children + flex_direction: FlexDirection::RowReverse, + } + on(|ev: On>, mut commands: Commands| { + commands.trigger(MenuEvent { source: ev.entity, action: MenuAction::Toggle }); + }) + [ + :icon(icons::CHEVRON_DOWN), + Node { + flex_grow: 0.2, + } + ] + } +} + +/// Menu Popup scene function +pub fn menu_popup() -> impl Scene { + bsn! { + Node { + position_type: PositionType::Absolute, + display: Display::Flex, + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Stretch, + align_items: AlignItems::Stretch, + border: UiRect::all(Val::Px(1.0)), + padding: UiRect::all(Val::Px(4.0)), + } + MenuPopupStyle + MenuPopup + template_value(Visibility::Hidden) + template_value(RoundedCorners::All.to_border_radius(4.0)) + ThemeBackgroundColor(tokens::MENU_BG) + ThemeBorderColor(tokens::MENU_BORDER) + BoxShadow::new( + Srgba::BLACK.with_alpha(0.9).into(), + Val::Px(0.0), + Val::Px(0.0), + Val::Px(1.0), + Val::Px(4.0), + ) + GlobalZIndex(100) + template_value( + Popover { + positions: vec![ + PopoverPlacement { + side: PopoverSide::Bottom, + align: PopoverAlign::Start, + gap: 2.0, + }, + PopoverPlacement { + side: PopoverSide::Top, + align: PopoverAlign::Start, + gap: 2.0, + }, + ], + window_margin: 10.0, + } + ) + OverrideClip + } +} + +/// Menu item scene function +pub fn menu_item() -> impl Scene { + bsn! { + Node { + height: size::ROW_HEIGHT, + min_width: size::ROW_HEIGHT, + justify_content: JustifyContent::Start, + align_items: AlignItems::Center, + padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)), + } + MenuItemStyle + MenuItem + Hovered + // TODO: port CursonIcon to GetTemplate + // CursorIcon::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeBackgroundColor(tokens::MENU_BG) // Same as menu + ThemeFontColor(tokens::MENUITEM_TEXT) + InheritableFont { + font: fonts::REGULAR, + font_size: 14.0, + } + } +} + +fn update_menuitem_styles( + q_menuitems: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeBackgroundColor, + &ThemeFontColor, + ), + ( + With, + Or<(Changed, Added, Added)>, + ), + >, + mut commands: Commands, +) { + for (button_ent, disabled, pressed, hovered, bg_color, font_color) in q_menuitems.iter() { + set_menuitem_colors( + button_ent, + disabled, + pressed, + hovered.0, + bg_color, + font_color, + &mut commands, + ); + } +} + +fn update_menuitem_styles_remove( + q_menuitems: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeBackgroundColor, + &ThemeFontColor, + ), + With, + >, + mut removed_disabled: RemovedComponents, + mut removed_pressed: RemovedComponents, + mut commands: Commands, +) { + removed_disabled + .read() + .chain(removed_pressed.read()) + .for_each(|ent| { + if let Ok((button_ent, disabled, pressed, hovered, bg_color, font_color)) = + q_menuitems.get(ent) + { + set_menuitem_colors( + button_ent, + disabled, + pressed, + hovered.0, + bg_color, + font_color, + &mut commands, + ); + } + }); +} + +fn set_menuitem_colors( + button_ent: Entity, + disabled: bool, + pressed: bool, + hovered: bool, + bg_color: &ThemeBackgroundColor, + font_color: &ThemeFontColor, + commands: &mut Commands, +) { + let bg_token = match (pressed, hovered) { + (true, _) => tokens::MENUITEM_BG_PRESSED, + (false, true) => tokens::MENUITEM_BG_HOVER, + (false, false) => tokens::MENU_BG, + }; + + let font_color_token = match disabled { + true => tokens::MENUITEM_TEXT_DISABLED, + false => tokens::MENUITEM_TEXT, + }; + + // Change background color + if bg_color.0 != bg_token { + commands + .entity(button_ent) + .insert(ThemeBackgroundColor(bg_token)); + } + + // Change font color + if font_color.0 != font_color_token { + commands + .entity(button_ent) + .insert(ThemeFontColor(font_color_token)); + } +} + +/// Plugin which registers the systems for updating the menu and menu button styles. +pub struct MenuPlugin; + +impl Plugin for MenuPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + (update_menuitem_styles, update_menuitem_styles_remove).in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/controls/mod.rs b/crates/bevy_feathers/src/controls/mod.rs index 4475911d17adb..aacfb30923eef 100644 --- a/crates/bevy_feathers/src/controls/mod.rs +++ b/crates/bevy_feathers/src/controls/mod.rs @@ -5,6 +5,7 @@ mod button; mod checkbox; mod color_slider; mod color_swatch; +mod menu; mod radio; mod slider; mod toggle_switch; @@ -14,6 +15,7 @@ pub use button::*; pub use checkbox::*; pub use color_slider::*; pub use color_swatch::*; +pub use menu::*; pub use radio::*; pub use slider::*; pub use toggle_switch::*; @@ -31,6 +33,7 @@ impl Plugin for ControlsPlugin { ButtonPlugin, CheckboxPlugin, ColorSliderPlugin, + MenuPlugin, RadioPlugin, SliderPlugin, ToggleSwitchPlugin, diff --git a/crates/bevy_feathers/src/dark_theme.rs b/crates/bevy_feathers/src/dark_theme.rs index 4a7248852c51a..31a6ab3719ca6 100644 --- a/crates/bevy_feathers/src/dark_theme.rs +++ b/crates/bevy_feathers/src/dark_theme.rs @@ -95,6 +95,16 @@ pub fn create_dark_theme() -> ThemeProps { tokens::SWITCH_SLIDE_DISABLED, palette::LIGHT_GRAY_2.with_alpha(0.3), ), + // Menus + (tokens::MENU_BG, palette::GRAY_1), + (tokens::MENU_BORDER, palette::WARM_GRAY_1), + (tokens::MENUITEM_BG_HOVER, palette::GRAY_1.lighter(0.05)), + (tokens::MENUITEM_BG_PRESSED, palette::GRAY_1.lighter(0.1)), + (tokens::MENUITEM_TEXT, palette::WHITE), + ( + tokens::MENUITEM_TEXT_DISABLED, + palette::WHITE.with_alpha(0.5), + ), ]), } } diff --git a/crates/bevy_feathers/src/icon.rs b/crates/bevy_feathers/src/icon.rs new file mode 100644 index 0000000000000..29a93520045ae --- /dev/null +++ b/crates/bevy_feathers/src/icon.rs @@ -0,0 +1,18 @@ +//! BSN Template for loading images and displaying them as [`ImageNodes`]. +use bevy_asset::AssetServer; +use bevy_ecs::template::template; +use bevy_scene2::{bsn, Scene}; +use bevy_ui::{widget::ImageNode, Node, Val}; + +/// Template which displays an icon. +pub fn icon(image: &'static str) -> impl Scene { + bsn! { + Node { + height: Val::Px(14.0), + } + template(move |entity| { + let handle = entity.resource::().load(image); + Ok(ImageNode::new(handle)) + }) + } +} diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs index 348b677f98e53..7752662f5fb18 100644 --- a/crates/bevy_feathers/src/lib.rs +++ b/crates/bevy_feathers/src/lib.rs @@ -18,6 +18,8 @@ //! Please report issues, submit fixes and propose changes. //! Thanks for stress-testing; let's build something better together. +extern crate alloc; + use bevy_app::{ HierarchyPropagatePlugin, Plugin, PluginGroup, PluginGroupBuilder, PostUpdate, PropagateSet, }; @@ -43,11 +45,14 @@ pub mod cursor; pub mod dark_theme; pub mod font_styles; pub mod handle_or_path; +mod icon; pub mod palette; pub mod rounded_corners; pub mod theme; pub mod tokens; +pub use icon::icon; + /// Plugin which installs observers and systems for feathers themes, cursors, and all controls. pub struct FeathersPlugin; @@ -62,6 +67,11 @@ impl Plugin for FeathersPlugin { embedded_asset!(app, "assets/fonts/FiraSans-Italic.ttf"); embedded_asset!(app, "assets/fonts/FiraMono-Medium.ttf"); + // Embedded icons + embedded_asset!(app, "assets/icons/chevron-down.png"); + embedded_asset!(app, "assets/icons/chevron-right.png"); + embedded_asset!(app, "assets/icons/x.png"); + // Embedded shader embedded_asset!(app, "assets/shaders/alpha_pattern.wgsl"); diff --git a/crates/bevy_feathers/src/tokens.rs b/crates/bevy_feathers/src/tokens.rs index a00a78bc799c0..9380b809799ee 100644 --- a/crates/bevy_feathers/src/tokens.rs +++ b/crates/bevy_feathers/src/tokens.rs @@ -137,3 +137,19 @@ pub const SWITCH_SLIDE: ThemeToken = ThemeToken::new_static("feathers.switch.sli /// Switch slide (disabled) pub const SWITCH_SLIDE_DISABLED: ThemeToken = ThemeToken::new_static("feathers.switch.slide.disabled"); + +// Menus + +/// Menu background +pub const MENU_BG: ThemeToken = ThemeToken::new_static("feathers.menu.bg"); +/// Menu border +pub const MENU_BORDER: ThemeToken = ThemeToken::new_static("feathers.menu.border"); +/// Menu item hovered +pub const MENUITEM_BG_HOVER: ThemeToken = ThemeToken::new_static("feathers.menuitem.bg.hover"); +/// Menu item pressed +pub const MENUITEM_BG_PRESSED: ThemeToken = ThemeToken::new_static("feathers.menuitem.bg.pressed"); +/// Menu item text +pub const MENUITEM_TEXT: ThemeToken = ThemeToken::new_static("feathers.menuitem.text"); +/// Menu item text (disabled) +pub const MENUITEM_TEXT_DISABLED: ThemeToken = + ThemeToken::new_static("feathers.menuitem.text.disabled"); diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index a490d00dda10e..e112c84d5ea26 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -167,12 +167,12 @@ pub struct TabNavigation<'w, 's> { } impl TabNavigation<'_, '_> { - /// Navigate to the desired focusable entity. + /// Navigate to the desired focusable entity, relative to the current focused entity. /// /// Change the [`NavAction`] to navigate in a different direction. /// Focusable entities are determined by the presence of the [`TabIndex`] component. /// - /// If no focusable entities are found, then this function will return either the first + /// If there is no currently focused entity, then this function will return either the first /// or last focusable entity, depending on the direction of navigation. For example, if /// `action` is `Next` and no focusable entities are found, then this function will return /// the first focusable entity. @@ -199,13 +199,46 @@ impl TabNavigation<'_, '_> { }) }); + self.navigate_internal(focus.0, action, tabgroup) + } + + /// Initialize focus to a focusable child of a container, either the first or last + /// depending on [`NavAction`]. This assumes that the parent entity has a [`TabGroup`] + /// component. + /// + /// Focusable entities are determined by the presence of the [`TabIndex`] component. + pub fn initialize( + &self, + parent: Entity, + action: NavAction, + ) -> Result { + // If there are no tab groups, then there are no focusable entities. + if self.tabgroup_query.is_empty() { + return Err(TabNavigationError::NoTabGroups); + } + + // Look for the tab group on the parent entity. + match self.tabgroup_query.get(parent) { + Ok(tabgroup) => self.navigate_internal(None, action, Some((parent, tabgroup.1))), + Err(_) => Err(TabNavigationError::NoTabGroups), + } + } + + pub fn navigate_internal( + &self, + focus: Option, + action: NavAction, + tabgroup: Option<(Entity, &TabGroup)>, + ) -> Result { let navigation_result = self.navigate_in_group(tabgroup, focus, action); match navigation_result { Ok(entity) => { - if focus.0.is_some() && tabgroup.is_none() { + if let Some(previous_focus) = focus + && tabgroup.is_none() + { Err(TabNavigationError::NoTabGroupForCurrentFocus { - previous_focus: focus.0.unwrap(), + previous_focus, new_focus: entity, }) } else { @@ -219,7 +252,7 @@ impl TabNavigation<'_, '_> { fn navigate_in_group( &self, tabgroup: Option<(Entity, &TabGroup)>, - focus: &InputFocus, + focus: Option, action: NavAction, ) -> Result { // List of all focusable entities found. @@ -269,7 +302,7 @@ impl TabNavigation<'_, '_> { } }); - let index = focusable.iter().position(|e| Some(e.0) == focus.0); + let index = focusable.iter().position(|e| Some(e.0) == focus); let count = focusable.len(); let next = match (index, action) { (Some(idx), NavAction::Next) => (idx + 1).rem_euclid(count), diff --git a/crates/bevy_math/src/rects/rect.rs b/crates/bevy_math/src/rects/rect.rs index 92b7059945949..eda0510a76978 100644 --- a/crates/bevy_math/src/rects/rect.rs +++ b/crates/bevy_math/src/rects/rect.rs @@ -356,6 +356,38 @@ impl Rect { } } + /// Return the area of this rectangle. + /// + /// # Examples + /// + /// ``` + /// # use bevy_math::Rect; + /// let r = Rect::new(0., 0., 10., 10.); // w=10 h=10 + /// assert_eq!(r.area(), 100.0); + /// ``` + #[inline] + pub fn area(&self) -> f32 { + self.width() * self.height() + } + + /// Scale this rect by a multiplicative factor + /// + /// # Examples + /// + /// ``` + /// # use bevy_math::Rect; + /// let r = Rect::new(1., 1., 2., 2.); // w=10 h=10 + /// assert_eq!(r.scale(2.).min.x, 2.0); + /// assert_eq!(r.scale(2.).max.x, 4.0); + /// ``` + #[inline] + pub fn scale(&self, factor: f32) -> Rect { + Self { + min: self.min * factor, + max: self.max * factor, + } + } + /// Returns self as [`IRect`] (i32) #[inline] pub fn as_irect(&self) -> IRect { diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index cb29d5b07fafe..8100d72b16eb7 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -2244,7 +2244,7 @@ pub struct CalculatedClip { /// UI node entities with this component will ignore any clipping rect they inherit, /// the node will not be clipped regardless of its ancestors' `Overflow` setting. -#[derive(Component)] +#[derive(Component, Default, Clone)] pub struct OverrideClip; #[expect( diff --git a/crates/bevy_ui_widgets/Cargo.toml b/crates/bevy_ui_widgets/Cargo.toml index 38ae0753f6a6a..f2585fdfbbdd1 100644 --- a/crates/bevy_ui_widgets/Cargo.toml +++ b/crates/bevy_ui_widgets/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["bevy"] # bevy bevy_app = { path = "../bevy_app", version = "0.17.1" } bevy_a11y = { path = "../bevy_a11y", version = "0.17.1" } +bevy_camera = { path = "../bevy_camera", version = "0.17.1" } bevy_ecs = { path = "../bevy_ecs", version = "0.17.1" } bevy_input = { path = "../bevy_input", version = "0.17.1" } bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.1" } diff --git a/crates/bevy_ui_widgets/src/lib.rs b/crates/bevy_ui_widgets/src/lib.rs index 3ab239412775e..8f30e2842701a 100644 --- a/crates/bevy_ui_widgets/src/lib.rs +++ b/crates/bevy_ui_widgets/src/lib.rs @@ -20,13 +20,16 @@ mod button; mod checkbox; +mod menu; mod observe; +pub mod popover; mod radio; mod scrollbar; mod slider; pub use button::*; pub use checkbox::*; +pub use menu::*; pub use observe::*; pub use radio::*; pub use scrollbar::*; @@ -35,6 +38,8 @@ pub use slider::*; use bevy_app::{PluginGroup, PluginGroupBuilder}; use bevy_ecs::{entity::Entity, event::EntityEvent}; +use crate::popover::PopoverPlugin; + /// A plugin group that registers the observers for all of the widgets in this crate. If you don't want to /// use all of the widgets, you can import the individual widget plugins instead. pub struct UiWidgetsPlugins; @@ -42,8 +47,10 @@ pub struct UiWidgetsPlugins; impl PluginGroup for UiWidgetsPlugins { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::() + .add(PopoverPlugin) .add(ButtonPlugin) .add(CheckboxPlugin) + .add(MenuPlugin) .add(RadioGroupPlugin) .add(ScrollbarPlugin) .add(SliderPlugin) diff --git a/crates/bevy_ui_widgets/src/menu.rs b/crates/bevy_ui_widgets/src/menu.rs new file mode 100644 index 0000000000000..098ba81a3ff2c --- /dev/null +++ b/crates/bevy_ui_widgets/src/menu.rs @@ -0,0 +1,377 @@ +//! Core widget components for menus and menu buttons. + +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::{ + component::Component, + entity::Entity, + event::EntityEvent, + hierarchy::ChildOf, + observer::On, + query::{Has, With, Without}, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res, ResMut}, + template::GetTemplate, +}; +use bevy_input::{ + keyboard::{KeyCode, KeyboardInput}, + ButtonState, +}; +use bevy_input_focus::{ + tab_navigation::{NavAction, TabGroup, TabNavigation}, + FocusedInput, InputFocus, +}; +use bevy_log::warn; +use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; +use bevy_ui::{InteractionDisabled, Pressed}; + +use crate::Activate; + +/// Action type for [`MenuEvent`]. +#[derive(Clone, Copy, Debug)] +pub enum MenuAction { + /// Indicates we want to open the menu, if it is not already open. + Open, + /// Open the menu if it's closed, close it if it's open. Generally sent from a menu button. + Toggle, + /// Close the menu and despawn it. Despawning may not happen immediately if there is a closing + /// transition animation. + Close, + /// Close the entire menu stack. + CloseAll, + /// Set focus to the menu button or other owner of the popup stack. This happens when + /// the escape key is pressed. + FocusRoot, +} + +/// Event used to control the state of the open menu. This bubbles upwards from the menu items +/// and the menu container, through the portal relation, and to the menu owner entity. +/// +/// Focus navigation: the menu may be part of a composite of multiple menus such as a menu bar. +/// This means that depending on direction, focus movement may move to the next menu item, or +/// the next menu. This also means that different events will often be handled at different +/// levels of the hierarchy - some being handled by the popup, and some by the popup's owner. +#[derive(EntityEvent, Clone, Debug)] +#[entity_event(propagate, auto_propagate)] +pub struct MenuEvent { + /// The [`MenuItem`] or [`MenuPopup`] that triggered this event. + #[event_target] + pub source: Entity, + + /// The desired action in response to this event. + pub action: MenuAction, +} + +/// Specifies the layout direction of the menu, for keyboard navigation +#[derive(Default, Debug, Clone, PartialEq)] +pub enum MenuLayout { + /// A vertical stack. Up and down arrows to move between items. + #[default] + Column, + /// A horizontal row. Left and right arrows to move between items. + Row, + /// A 2D grid. Arrow keys are not mapped, you'll need to write your own observer. + Grid, +} + +/// Component that defines a popup menu container. +/// +/// A popup menu *must* contain at least one focusable entity. The first such entity will acquire +/// focus when the popup is spawned; arrow keys can be used to navigate between menu items. If no +/// descendant of the menu has focus, the menu will automatically close. This rule has several +/// consequences: +/// +/// * Clicking on another widget or empty space outside the menu will cause the menu to close. +/// * Two menus cannot be displayed at the same time unless one is an ancestor of the other. +#[derive(Component, Debug, Default, Clone)] +#[require( + AccessibilityNode(accesskit::Node::new(Role::MenuListPopup)), + TabGroup::modal() +)] +#[require(MenuAcquireFocus)] +pub struct MenuPopup { + /// The layout orientation of the menu + pub layout: MenuLayout, +} + +/// Component that defines a menu item. +#[derive(Component, Debug, Clone, GetTemplate)] +#[require(AccessibilityNode(accesskit::Node::new(Role::MenuItem)))] +pub struct MenuItem; + +/// Marker component that indicates that we need to set focus to the first menu item. +#[derive(Component, Debug, Default)] +struct MenuAcquireFocus; + +/// Component that indicates that the menu is closing. +#[derive(Component, Debug, Default)] +struct MenuClosing; + +fn menu_acquire_focus( + q_menus: Query, With)>, + mut focus: ResMut, + tab_navigation: TabNavigation, + mut commands: Commands, +) { + for menu in q_menus.iter() { + // When a menu is spawned, attempt to find the first focusable menu item, and set focus + // to it. + match tab_navigation.initialize(menu, NavAction::First) { + Ok(next) => { + commands.entity(menu).remove::(); + focus.0 = Some(next); + } + Err(e) => { + warn!( + "No focusable menu items for popup menu: {}, error: {:?}", + menu, e + ); + } + } + } +} + +fn menu_on_lose_focus( + q_menus: Query< + Entity, + ( + With, + Without, + Without, + ), + >, + q_parent: Query<&ChildOf>, + focus: Res, + mut commands: Commands, +) { + // Close any menu which doesn't contain the focus entity. + for menu in q_menus.iter() { + // TODO: Change this logic when we support submenus. Don't want to send multiple close + // events. Perhaps what we can do is add `CoreMenuClosing` to the whole stack. + let contains_focus = match focus.0 { + Some(focus_ent) => { + focus_ent == menu || q_parent.iter_ancestors(focus_ent).any(|ent| ent == menu) + } + None => false, + }; + + if !contains_focus { + commands.entity(menu).insert(MenuClosing); + commands.trigger(MenuEvent { + source: menu, + action: MenuAction::CloseAll, + }); + } + } +} + +fn menu_on_key_event( + mut ev: On>, + q_item: Query, With>, + q_menu: Query<&MenuPopup>, + tab_navigation: TabNavigation, + mut focus: ResMut, + mut commands: Commands, +) { + if let Ok(disabled) = q_item.get(ev.focused_entity) { + if !disabled { + let event = &ev.event().input; + if !event.repeat && event.state == ButtonState::Pressed { + match event.key_code { + // Activate the item and close the popup + KeyCode::Enter | KeyCode::Space => { + ev.propagate(false); + // Trigger the menu action + commands.trigger(Activate { + entity: ev.event().focused_entity, + }); + // Set the focus to the menu button. + commands.trigger(MenuEvent { + source: ev.event().focused_entity, + action: MenuAction::FocusRoot, + }); + // Close the stack + commands.trigger(MenuEvent { + source: ev.event().focused_entity, + action: MenuAction::CloseAll, + }); + } + + _ => (), + } + } + } + } else if let Ok(menu) = q_menu.get(ev.focused_entity) { + let event = &ev.event().input; + if !event.repeat && event.state == ButtonState::Pressed { + match event.key_code { + // Close the popup + KeyCode::Escape => { + ev.propagate(false); + // Set the focus to the menu button. + commands.trigger(MenuEvent { + source: ev.focused_entity, + action: MenuAction::FocusRoot, + }); + // Close the stack + commands.trigger(MenuEvent { + source: ev.focused_entity, + action: MenuAction::CloseAll, + }); + } + + // Focus the adjacent item in the up direction + KeyCode::ArrowUp => { + if menu.layout == MenuLayout::Column { + ev.propagate(false); + focus.0 = tab_navigation.navigate(&focus, NavAction::Previous).ok(); + } + } + + // Focus the adjacent item in the down direction + KeyCode::ArrowDown => { + if menu.layout == MenuLayout::Column { + ev.propagate(false); + focus.0 = tab_navigation.navigate(&focus, NavAction::Next).ok(); + } + } + + // Focus the adjacent item in the left direction + KeyCode::ArrowLeft => { + if menu.layout == MenuLayout::Row { + ev.propagate(false); + focus.0 = tab_navigation.navigate(&focus, NavAction::Previous).ok(); + } + } + + // Focus the adjacent item in the right direction + KeyCode::ArrowRight => { + if menu.layout == MenuLayout::Row { + ev.propagate(false); + focus.0 = tab_navigation.navigate(&focus, NavAction::Next).ok(); + } + } + + // Focus the first item + KeyCode::Home => { + ev.propagate(false); + focus.0 = tab_navigation.navigate(&focus, NavAction::First).ok(); + } + + // Focus the last item + KeyCode::End => { + ev.propagate(false); + focus.0 = tab_navigation.navigate(&focus, NavAction::Last).ok(); + } + + _ => (), + } + } + } +} + +fn menu_item_on_pointer_click( + mut ev: On>, + mut q_state: Query<(Has, Has), With>, + mut commands: Commands, +) { + if let Ok((pressed, disabled)) = q_state.get_mut(ev.entity) { + ev.propagate(false); + if pressed && !disabled { + // Trigger the menu action. + commands.trigger(Activate { entity: ev.entity }); + // Set the focus to the menu button. + commands.trigger(MenuEvent { + source: ev.entity, + action: MenuAction::FocusRoot, + }); + // Close the stack + commands.trigger(MenuEvent { + source: ev.entity, + action: MenuAction::CloseAll, + }); + } + } +} + +fn menu_item_on_pointer_down( + mut ev: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((item, disabled, pressed)) = q_state.get_mut(ev.entity) { + ev.propagate(false); + if !disabled && !pressed { + commands.entity(item).insert(Pressed); + } + } +} + +fn menu_item_on_pointer_up( + mut ev: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((item, disabled, pressed)) = q_state.get_mut(ev.entity) { + ev.propagate(false); + if !disabled && pressed { + commands.entity(item).remove::(); + } + } +} + +fn menu_item_on_pointer_drag_end( + mut ev: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((item, disabled, pressed)) = q_state.get_mut(ev.entity) { + ev.propagate(false); + if !disabled && pressed { + commands.entity(item).remove::(); + } + } +} + +fn menu_item_on_pointer_cancel( + mut ev: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((item, disabled, pressed)) = q_state.get_mut(ev.entity) { + ev.propagate(false); + if !disabled && pressed { + commands.entity(item).remove::(); + } + } +} + +fn menu_on_menu_event( + mut ev: On, + q_popup: Query<(), With>, + mut commands: Commands, +) { + if q_popup.contains(ev.source) { + if let MenuAction::Close = ev.event().action { + ev.propagate(false); + commands.entity(ev.source).despawn(); + } + } +} + +/// Plugin that adds the observers for the [`CoreMenuItem`] widget. +pub struct MenuPlugin; + +impl Plugin for MenuPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, (menu_acquire_focus, menu_on_lose_focus).chain()) + .add_observer(menu_on_key_event) + .add_observer(menu_on_menu_event) + .add_observer(menu_item_on_pointer_down) + .add_observer(menu_item_on_pointer_up) + .add_observer(menu_item_on_pointer_click) + .add_observer(menu_item_on_pointer_drag_end) + .add_observer(menu_item_on_pointer_cancel); + } +} diff --git a/crates/bevy_ui_widgets/src/popover.rs b/crates/bevy_ui_widgets/src/popover.rs new file mode 100644 index 0000000000000..73591f715dca2 --- /dev/null +++ b/crates/bevy_ui_widgets/src/popover.rs @@ -0,0 +1,246 @@ +//! Framework for positioning of popups, tooltips, and other popover UI elements. + +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_camera::visibility::Visibility; +use bevy_ecs::{ + change_detection::DetectChangesMut, component::Component, hierarchy::ChildOf, query::Without, + schedule::IntoScheduleConfigs, system::Query, +}; +use bevy_math::{Rect, Vec2}; +use bevy_ui::{ + ComputedNode, ComputedUiRenderTargetInfo, Node, PositionType, UiGlobalTransform, UiSystems, Val, +}; + +/// Which side of the parent element the popover element should be placed. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub enum PopoverSide { + /// The popover element should be placed above the parent. + Top, + /// The popover element should be placed below the parent. + #[default] + Bottom, + /// The popover element should be placed to the left of the parent. + Left, + /// The popover element should be placed to the right of the parent. + Right, +} + +impl PopoverSide { + /// Returns the side that is the mirror image of this side. + pub fn mirror(&self) -> Self { + match self { + PopoverSide::Top => PopoverSide::Bottom, + PopoverSide::Bottom => PopoverSide::Top, + PopoverSide::Left => PopoverSide::Right, + PopoverSide::Right => PopoverSide::Left, + } + } +} + +/// How the popover element should be aligned to the parent element. The alignment will be along an +/// axis that is perpendicular to the direction of the popover side. So for example, if the popup is +/// positioned below the parent, then the [`PopoverAlign`] variant controls the horizontal aligment +/// of the popup. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub enum PopoverAlign { + /// The starting edge of the popover element should be aligned to the starting edge of the + /// parent. + #[default] + Start, + /// The ending edge of the popover element should be aligned to the ending edge of the parent. + End, + /// The center of the popover element should be aligned to the center of the parent. + Center, +} + +/// Indicates a possible position of a popover element relative to it's parent. You can +/// specify multiple possible positions; the positioning code will check to see if there is +/// sufficient space to display the popup without clipping. If any position has sufficient room, +/// it will pick the first one; if there are none, then it will pick the least bad one. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct PopoverPlacement { + /// The side of the parent entity where the popover element should be placed. + pub side: PopoverSide, + + /// How the popover element should be aligned to the parent entity. + pub align: PopoverAlign, + + /// The size of the gap between the parent and the popover element, in logical pixels. This will + /// offset the popover along the direction of [`side`]. + pub gap: f32, +} + +/// Component which is inserted into a popover element to make it dynamically position relative to +/// an parent element. +#[derive(Component, PartialEq, Default)] +pub struct Popover { + /// List of potential positions for the popover element relative to the parent. + pub positions: Vec, + + /// Indicates how close to the window edge the popup is allowed to go. + pub window_margin: f32, +} + +impl Clone for Popover { + fn clone(&self) -> Self { + Self { + positions: self.positions.clone(), + window_margin: self.window_margin, + } + } +} + +fn position_popover( + mut q_popover: Query<( + &mut Node, + &mut Visibility, + &ComputedNode, + &ComputedUiRenderTargetInfo, + &Popover, + &ChildOf, + )>, + q_parent: Query<(&ComputedNode, &UiGlobalTransform), Without>, +) { + for (mut node, mut visibility, computed_node, computed_target, popover, parent) in + q_popover.iter_mut() + { + // A rectangle which represents the area of the window. + let window_rect = Rect { + min: Vec2::ZERO, + max: computed_target.logical_size(), + } + .inflate(-popover.window_margin); + + // Compute the parent rectangle. + let Ok((parent_node, parent_transform)) = q_parent.get(parent.parent()) else { + continue; + }; + // Computed node size includes the border, but since absolute positioning doesn't include + // border we need to remove it from the calculations. + let parent_size = parent_node.size() + - Vec2::new( + parent_node.border.left + parent_node.border.right, + parent_node.border.top + parent_node.border.bottom, + ); + let parent_rect = Rect::from_center_size(parent_transform.translation, parent_size) + .scale(parent_node.inverse_scale_factor); + + let mut best_occluded = f32::MAX; + let mut best_rect = Rect::default(); + + // Loop through all the potential positions and find a good one. + for position in &popover.positions { + let popover_size = computed_node.size() * computed_node.inverse_scale_factor; + let mut rect = Rect::default(); + + let target_width = popover_size.x; + let target_height = popover_size.y; + + // Position along main axis. + match position.side { + PopoverSide::Top => { + rect.max.y = parent_rect.min.y - position.gap; + rect.min.y = rect.max.y - popover_size.y; + } + + PopoverSide::Bottom => { + rect.min.y = parent_rect.max.y + position.gap; + rect.max.y = rect.min.y + popover_size.y; + } + + PopoverSide::Left => { + rect.max.x = parent_rect.min.x - position.gap; + rect.min.x = rect.max.x - popover_size.x; + } + + PopoverSide::Right => { + rect.min.x = parent_rect.max.x + position.gap; + rect.max.x = rect.min.x + popover_size.x; + } + } + + // Position along secondary axis. + match position.align { + PopoverAlign::Start => match position.side { + PopoverSide::Top | PopoverSide::Bottom => { + rect.min.x = parent_rect.min.x; + rect.max.x = rect.min.x + target_width; + } + + PopoverSide::Left | PopoverSide::Right => { + rect.min.y = parent_rect.min.y; + rect.max.y = rect.min.y + target_height; + } + }, + + PopoverAlign::End => match position.side { + PopoverSide::Top | PopoverSide::Bottom => { + rect.max.x = parent_rect.max.x; + rect.min.x = rect.max.x - target_width; + } + + PopoverSide::Left | PopoverSide::Right => { + rect.max.y = parent_rect.max.y; + rect.min.y = rect.max.y - target_height; + } + }, + + PopoverAlign::Center => match position.side { + PopoverSide::Top | PopoverSide::Bottom => { + rect.min.x = (parent_rect.width() - target_width) * 0.5; + rect.max.x = rect.min.x + target_width; + } + + PopoverSide::Left | PopoverSide::Right => { + rect.min.y = (parent_rect.width() - target_height) * 0.5; + rect.max.y = rect.min.y + target_height; + } + }, + } + + // Clip to window and see how much of the popover element is occluded. We can calculate + // how much was clipped by intersecting the rectangle against the window bounds, and + // then subtracting the area from the area of the unclipped rectangle. + let clipped_rect = rect.intersect(window_rect); + let occlusion = rect.area() - clipped_rect.area(); + + // Find the position that has the least occlusion. + if occlusion < best_occluded { + best_occluded = occlusion; + best_rect = rect; + } + } + + // Update node properties, but only if they are different from before (to avoid setting + // change detection bit). + if best_occluded < f32::MAX { + let left = Val::Px(best_rect.min.x - parent_rect.min.x); + let top = Val::Px(best_rect.min.y - parent_rect.min.y); + visibility.set_if_neq(Visibility::Visible); + if node.left != left { + node.left = left; + } + if node.top != top { + node.top = top; + } + if node.bottom != Val::DEFAULT { + node.bottom = Val::DEFAULT; + } + if node.right != Val::DEFAULT { + node.right = Val::DEFAULT; + } + if node.position_type != PositionType::Absolute { + node.position_type = PositionType::Absolute; + } + } + } +} + +/// Plugin that adds systems for the [`Popover`] component. +pub struct PopoverPlugin; + +impl Plugin for PopoverPlugin { + fn build(&self, app: &mut App) { + app.add_systems(PostUpdate, position_popover.in_set(UiSystems::Prepare)); + } +} diff --git a/examples/ui/feathers.rs b/examples/ui/feathers.rs index dae81bfef153c..78cc973fd6ab8 100644 --- a/examples/ui/feathers.rs +++ b/examples/ui/feathers.rs @@ -4,9 +4,9 @@ use bevy::{ color::palettes, feathers::{ controls::{ - button, checkbox, color_slider, color_swatch, radio, slider, toggle_switch, - ButtonProps, ButtonVariant, ColorChannel, ColorSlider, ColorSliderProps, ColorSwatch, - SliderBaseColor, SliderProps, + button, checkbox, color_slider, color_swatch, menu, menu_button, menu_item, menu_popup, + radio, slider, toggle_switch, ButtonProps, ButtonVariant, ColorChannel, ColorSlider, + ColorSliderProps, ColorSwatch, MenuButtonProps, SliderBaseColor, SliderProps, }, dark_theme::create_dark_theme, rounded_corners::RoundedCorners, @@ -22,6 +22,7 @@ use bevy::{ SliderPrecision, SliderStep, SliderValue, ValueChange, }, }; +use bevy_scene2::SpawnRelatedScenes as _; /// A struct to hold the state of various widgets shown in the demo. #[derive(Resource)] @@ -123,6 +124,40 @@ fn demo_root() -> impl Scene { }) [ (Text::new("Primary") ThemedText) ] ), + ( + menu(|parent| { + parent.spawn_related_scenes::(bsn_list!( + :menu_popup() + [ + ( + :menu_item() + on(|_: On| { + info!("Menu button clicked!"); + }) + [ + (Text("MenuItem") ThemedText) + ] + ), + ( + :menu_item() + on(|_: On| { + info!("Menu button clicked!"); + }) + [ + (Text("MenuItem") ThemedText) + ] + ) + ] + )); + }) [ + ( + :menu_button(MenuButtonProps::default()) + [ + (Text("Menu") ThemedText) + ] + ) + ] + ) ] ), ( From 01e62cf0622df919db44eabe42bf24e881b0bbe5 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 20 Oct 2025 14:50:51 -0700 Subject: [PATCH 12/13] Fix early despawn of scenes. Add documentation --- crates/bevy_asset/src/handle.rs | 8 +++ crates/bevy_ecs/src/template.rs | 68 +++++++++++++++----- crates/bevy_scene2/macros/src/bsn/codegen.rs | 2 +- crates/bevy_scene2/src/lib.rs | 2 +- crates/bevy_scene2/src/spawn.rs | 30 ++++----- examples/ui/feathers.rs | 1 - 6 files changed, 78 insertions(+), 33 deletions(-) diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index b4c8d1a477f0f..4d22e938b2576 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -7,6 +7,7 @@ use bevy_ecs::{ error::Result, template::{GetTemplate, Template, TemplateContext}, }; +use bevy_platform::collections::Equivalent; use bevy_reflect::{Reflect, TypePath}; use core::{ any::TypeId, @@ -254,6 +255,13 @@ impl Hash for Handle { } } +// Handle uses AssetId when hashing. This enables using AssetId instead of handle with hashsets and hashmaps. +impl Equivalent> for AssetId { + fn equivalent(&self, key: &Handle) -> bool { + *self == key.id() + } +} + impl PartialOrd for Handle { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) diff --git a/crates/bevy_ecs/src/template.rs b/crates/bevy_ecs/src/template.rs index bb1dc9a203265..730513bb61644 100644 --- a/crates/bevy_ecs/src/template.rs +++ b/crates/bevy_ecs/src/template.rs @@ -4,7 +4,7 @@ pub use bevy_ecs_macros::GetTemplate; use crate::{ bundle::Bundle, - entity::{Entities, Entity, EntityPath}, + entity::{Entity, EntityPath}, error::{BevyError, Result}, resource::Resource, world::{EntityWorldMut, World}, @@ -29,13 +29,20 @@ pub trait Template { fn register_data(&self, _data: &mut TemplateData) {} } +/// The context used to apply the current [`Template`]. This contains a reference to the entity that the template is being +/// applied to. pub struct TemplateContext<'a> { + /// The current entity the template is being applied to pub entity: &'a mut EntityWorldMut<'a>, + /// The scoped entities mapping for the current template context pub scoped_entities: &'a mut ScopedEntities, + /// The entity scopes for the current template context. This matches + /// the `scoped_entities`. pub entity_scopes: &'a EntityScopes, } impl<'a> TemplateContext<'a> { + /// Creates a new [`TemplateContext`]. pub fn new( entity: &'a mut EntityWorldMut<'a>, scoped_entities: &'a mut ScopedEntities, @@ -48,6 +55,8 @@ impl<'a> TemplateContext<'a> { } } + /// Retrieves the scoped entity if it has already been spawned, and spawns a new entity if it has not + /// yet been spawned. pub fn get_scoped_entity(&mut self, scope: usize, index: usize) -> Entity { self.scoped_entities.get( // SAFETY: this only uses the world to spawn an empty entity @@ -59,6 +68,8 @@ impl<'a> TemplateContext<'a> { } } +/// A mapping from from an entity reference's (scope, index) to a contiguous flat index that uniquely +/// identifies the entity within a scene. #[derive(Default, Debug)] pub struct EntityScopes { scopes: Vec>>, @@ -66,15 +77,21 @@ pub struct EntityScopes { } impl EntityScopes { + /// The number of entities defined across all scopes. #[inline] - pub fn entity_len(&self) -> usize { + pub fn entity_count(&self) -> usize { self.next_index } + + /// Allocate a new contiguous entity index for the given (scope, index) pair. pub fn alloc(&mut self, scope: usize, index: usize) { *self.get_mut(scope, index) = Some(self.next_index); self.next_index += 1; } + /// Assign an existing contiguous entity index for the given (scope, index) pair. + /// This is generally used when there are multiple (scope, index) pairs that point + /// to the same entity (ex: scene inheritance). pub fn assign(&mut self, scope: usize, index: usize, value: usize) { let option = self.get_mut(scope, index); *option = Some(value); @@ -92,10 +109,12 @@ impl EntityScopes { unsafe { indices.get_unchecked_mut(index) } } + /// Gets the assigned contiguous entity index for the given (scope, index) pair pub fn get(&self, scope: usize, index: usize) -> Option { *self.scopes.get(scope)?.get(index)? } + /// Creates a new scope and returns it. pub fn add_scope(&mut self) -> usize { let scope_index = self.scopes.len(); self.scopes.push(Vec::default()); @@ -103,16 +122,20 @@ impl EntityScopes { } } +/// A contiguous list of entities identfied by their index in the list. #[derive(Debug)] pub struct ScopedEntities(Vec>); impl ScopedEntities { + /// Creates a new [`ScopedEntities`] with the given `size`, intialized to [`None`] (no [`Entity`] assigned). pub fn new(size: usize) -> Self { Self(vec![None; size]) } } impl ScopedEntities { + /// Gets the [`Entity`] assigned to the given (scope, index) pair, if it exists, and spawns a new entity if + /// it does not. pub fn get( &mut self, world: &mut World, @@ -124,6 +147,7 @@ impl ScopedEntities { *self.0[index].get_or_insert_with(|| world.spawn_empty().id()) } + /// Assigns the given `entity` to the (scope, index) pair. pub fn set( &mut self, entity_scopes: &EntityScopes, @@ -137,6 +161,7 @@ impl ScopedEntities { } impl<'a> TemplateContext<'a> { + /// Retrieves a reference to the given resource `R`. pub fn resource(&self) -> &R { self.entity.resource() } @@ -184,9 +209,34 @@ pub struct TemplateTuple(pub T); all_tuples!(template_impl, 0, 12, T); +impl Template for T { + type Output = T; + + fn build(&mut self, _context: &mut TemplateContext) -> Result { + Ok(self.clone()) + } +} + +impl GetTemplate for T { + type Template = T; +} + +/// A [`Template`] reference to an [`Entity`]. pub enum EntityReference<'a> { + /// A reference to an entity via an [`EntityPath`] Path(EntityPath<'a>), - Index { scope: usize, index: usize }, + /// An entity index within the current [`TemplateContext`], which is defined by a scope + /// and an index. This references a specific (and sometimes yet-to-be-spawned) entity defined + /// within a given scope. + /// + /// In most cases this is initialized by the scene system and should not be initialized manually. + /// Scopes must be defined ahead of time on the [`TemplateContext`]. + Index { + /// The scope of the entity index. This must be defined ahead of time. + scope: usize, + /// The index that uniquely identifies the entity within the current scope. + index: usize, + }, } impl<'a> Default for EntityReference<'a> { @@ -211,18 +261,6 @@ impl GetTemplate for Entity { type Template = EntityReference<'static>; } -impl Template for T { - type Output = T; - - fn build(&mut self, _context: &mut TemplateContext) -> Result { - Ok(self.clone()) - } -} - -impl GetTemplate for T { - type Template = T; -} - /// A type-erased, object-safe, downcastable version of [`Template`]. pub trait ErasedTemplate: Downcast + Send + Sync { /// Applies this template to the given `entity`. diff --git a/crates/bevy_scene2/macros/src/bsn/codegen.rs b/crates/bevy_scene2/macros/src/bsn/codegen.rs index 9057142bea2a0..9859999ddc3d2 100644 --- a/crates/bevy_scene2/macros/src/bsn/codegen.rs +++ b/crates/bevy_scene2/macros/src/bsn/codegen.rs @@ -475,7 +475,7 @@ impl ToTokens for BsnValue { BsnValue::Type(ty) => { ty.to_tokens(tokens); } - BsnValue::Name(ident) => { + BsnValue::Name(_ident) => { // Name requires additional context to convert to tokens unreachable!() } diff --git a/crates/bevy_scene2/src/lib.rs b/crates/bevy_scene2/src/lib.rs index 11e623c9bf3cc..410324f978bcb 100644 --- a/crates/bevy_scene2/src/lib.rs +++ b/crates/bevy_scene2/src/lib.rs @@ -3,7 +3,7 @@ pub mod prelude { pub use crate::{ bsn, bsn_list, on, CommandsSpawnScene, LoadScene, PatchGetTemplate, PatchTemplate, Scene, - SceneList, ScenePatchInstance, SpawnScene, + SceneList, ScenePatchInstance, SpawnRelatedScenes, SpawnScene, }; } diff --git a/crates/bevy_scene2/src/spawn.rs b/crates/bevy_scene2/src/spawn.rs index 958960ed55601..3254abe4584ae 100644 --- a/crates/bevy_scene2/src/spawn.rs +++ b/crates/bevy_scene2/src/spawn.rs @@ -1,7 +1,7 @@ use crate::{ PatchContext, ResolvedScene, Scene, SceneList, SceneListPatch, ScenePatch, ScenePatchInstance, }; -use bevy_asset::{AssetEvent, AssetId, AssetServer, Assets, Handle}; +use bevy_asset::{AssetEvent, AssetServer, Assets, Handle}; use bevy_ecs::{ message::MessageCursor, prelude::*, @@ -136,8 +136,8 @@ pub fn resolve_scene_patches( #[derive(Resource, Default)] pub struct QueuedScenes { - waiting_entities: HashMap, Vec>, - waiting_list_entities: HashMap, Vec>, + waiting_entities: HashMap, Vec>, + waiting_list_entities: HashMap, Vec>, } struct SceneListSpawn { @@ -176,9 +176,9 @@ pub fn spawn_queued( 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 Ok(handle) = handles.get(world, entity).map(|h| &h.0) { if let Some((Some(scene), Some(entity_scopes))) = - patches.get_mut(id).map(|p| { + patches.get_mut(handle).map(|p| { (p.resolved.as_mut(), p.entity_scopes.as_ref()) }) { @@ -188,14 +188,16 @@ pub fn spawn_queued( .apply(&mut TemplateContext::new( &mut entity_mut, &mut ScopedEntities::new( - entity_scopes.entity_len(), + entity_scopes.entity_count(), ), entity_scopes, )) .unwrap(); } else { - let entities = - queued.waiting_entities.entry(id).or_default(); + let entities = queued + .waiting_entities + .entry(handle.clone()) + .or_default(); entities.push(entity); } } @@ -224,17 +226,15 @@ pub fn spawn_queued( .apply(&mut TemplateContext::new( &mut child_entity, &mut ScopedEntities::new( - entity_scopes.entity_len(), + entity_scopes.entity_count(), ), entity_scopes, )) .unwrap(); } } else { - let entities = queued - .waiting_list_entities - .entry(handle.id()) - .or_default(); + let entities = + queued.waiting_list_entities.entry(handle).or_default(); entities.push(scene_list_spawn); } } @@ -253,7 +253,7 @@ pub fn spawn_queued( .apply(&mut TemplateContext::new( &mut entity_mut, &mut ScopedEntities::new( - entity_scopes.entity_len(), + entity_scopes.entity_count(), ), entity_scopes, )) @@ -282,7 +282,7 @@ pub fn spawn_queued( .apply(&mut TemplateContext::new( &mut child_entity, &mut ScopedEntities::new( - entity_scopes.entity_len(), + entity_scopes.entity_count(), ), entity_scopes, )) diff --git a/examples/ui/feathers.rs b/examples/ui/feathers.rs index 78cc973fd6ab8..542d127d669e7 100644 --- a/examples/ui/feathers.rs +++ b/examples/ui/feathers.rs @@ -22,7 +22,6 @@ use bevy::{ SliderPrecision, SliderStep, SliderValue, ValueChange, }, }; -use bevy_scene2::SpawnRelatedScenes as _; /// A struct to hold the state of various widgets shown in the demo. #[derive(Resource)] From 8a401d271f645edf02e1b609ffaff3c4af0c2dc4 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 20 Oct 2025 14:57:12 -0700 Subject: [PATCH 13/13] Remove unnecessary {} around generated child expression code --- crates/bevy_scene2/macros/src/bsn/codegen.rs | 2 +- crates/bevy_scene2/macros/src/bsn/parse.rs | 3 ++- crates/bevy_scene2/macros/src/bsn/types.rs | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/bevy_scene2/macros/src/bsn/codegen.rs b/crates/bevy_scene2/macros/src/bsn/codegen.rs index 9859999ddc3d2..5df7f27da0c92 100644 --- a/crates/bevy_scene2/macros/src/bsn/codegen.rs +++ b/crates/bevy_scene2/macros/src/bsn/codegen.rs @@ -410,7 +410,7 @@ impl BsnSceneListItems { let tokens = bsn.to_tokens(bevy_scene, bevy_ecs, bevy_asset, entity_refs); quote! {#bevy_scene::EntityScene(#tokens)} } - BsnSceneListItem::Expression(block) => quote! {#block}, + BsnSceneListItem::Expression(statements) => quote! {#(#statements)*}, }); quote! { #bevy_scene::auto_nest_tuple!(#(#scenes),*) diff --git a/crates/bevy_scene2/macros/src/bsn/parse.rs b/crates/bevy_scene2/macros/src/bsn/parse.rs index 94854fde44ddd..28c2c31c48844 100644 --- a/crates/bevy_scene2/macros/src/bsn/parse.rs +++ b/crates/bevy_scene2/macros/src/bsn/parse.rs @@ -179,7 +179,8 @@ impl Parse for BsnSceneListItems { impl Parse for BsnSceneListItem { fn parse(input: ParseStream) -> Result { Ok(if input.peek(Brace) { - BsnSceneListItem::Expression(input.parse::()?) + let block = input.parse::()?; + BsnSceneListItem::Expression(block.stmts) } else { BsnSceneListItem::Scene(input.parse::>()?) }) diff --git a/crates/bevy_scene2/macros/src/bsn/types.rs b/crates/bevy_scene2/macros/src/bsn/types.rs index 938e282d4037e..6fab88e1d024f 100644 --- a/crates/bevy_scene2/macros/src/bsn/types.rs +++ b/crates/bevy_scene2/macros/src/bsn/types.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream; -use syn::{punctuated::Punctuated, Block, Expr, Ident, Lit, LitStr, Path, Token}; +use syn::{punctuated::Punctuated, Expr, Ident, Lit, LitStr, Path, Stmt, Token}; #[derive(Debug)] pub struct BsnRoot(pub Bsn); @@ -46,7 +46,7 @@ pub struct BsnSceneListItems(pub Vec); #[derive(Debug)] pub enum BsnSceneListItem { Scene(Bsn), - Expression(Block), + Expression(Vec), } #[derive(Debug)]