From f5e6ab45a689f7a9ef88d68b9fd8188b7416c717 Mon Sep 17 00:00:00 2001 From: Miles Silberling-Cook Date: Mon, 16 Dec 2024 16:46:51 -0500 Subject: [PATCH 1/8] Add basic template macro --- Cargo.toml | 3 + examples/super-sheep-counter-2000.rs | 100 ++++++++ src/lib.rs | 3 + src/template.rs | 358 +++++++++++++++++++++++++++ 4 files changed, 464 insertions(+) create mode 100644 examples/super-sheep-counter-2000.rs create mode 100644 src/template.rs diff --git a/Cargo.toml b/Cargo.toml index 16e3fcd..df80fc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,6 @@ tags = ["bevy", "ecs"] [dependencies] bevy_ecs = { version = "0.15", default-features = false } bevy_hierarchy = { version = "0.15", default-features = false } + +[dev-dependencies] +bevy = { version = "0.15" } diff --git a/examples/super-sheep-counter-2000.rs b/examples/super-sheep-counter-2000.rs new file mode 100644 index 0000000..6f34119 --- /dev/null +++ b/examples/super-sheep-counter-2000.rs @@ -0,0 +1,100 @@ +//! Super Sheep-Counter 2000 +//! +//! An all-in-one numerical ruminant package. + +use i_cant_believe_its_not_bsn::*; + +use bevy::color::palettes::css; +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(sheep_plugin) + .run(); +} + +fn sheep_plugin(app: &mut App) { + app.add_systems(Startup, setup) + .add_systems(Update, sheep_system) + .add_observer(observe_buttons); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d); +} + +#[derive(Component)] +struct Sheep; + +#[derive(Component)] +enum Button { + Increment, + Decrement, +} + +// A query that pulls data from the ecs and then updates it using a template. +fn sheep_system(mut commands: Commands, sheep: Query<&Sheep>) { + let num_sheep = sheep.iter().len(); + + let template = template!( + root: { + Node { + position_type: PositionType::Absolute, + bottom: Val::Px(5.0), + right: Val::Px(5.0), + ..default() + } + } [ + { counter(num_sheep, "sheep", Button::Increment, Button::Decrement) }; + ]; + ); + + commands.build(template); +} + +// A function that returns an ecs template. +fn counter(num: usize, name: &str, inc: T, dec: T) -> Template { + template! { + header: { Text::new("You have ") } [ + number: { TextSpan::new(format!("{num}")) }; + sheep: { TextSpan::new(format!(" {name}!")) }; + ]; + increase: {( + Button, Text::new("Increase"), TextColor(css::GREEN.into()), inc, visible_if(num < 100) + )}; + decrease: {( + Button, Text::new("Decrease"), TextColor(css::RED.into()), dec, visible_if(num > 0) + )}; + } +} + +// A component helper function for computing visibility. +fn visible_if(condition: bool) -> Visibility { + if condition { + Visibility::Visible + } else { + Visibility::Hidden + } +} + +// A global observer which responds to button clicks. +fn observe_buttons( + mut trigger: Trigger>, + buttons: Query<&Button>, + sheep: Query>, + mut commands: Commands, +) { + match buttons.get(trigger.target).ok() { + Some(Button::Increment) => { + commands.spawn(Sheep); + } + Some(Button::Decrement) => { + if let Some(sheep) = sheep.iter().next() { + commands.entity(sheep).despawn_recursive(); + } + } + _ => {} + } + trigger.propagate(false); +} diff --git a/src/lib.rs b/src/lib.rs index 949010d..541e062 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,6 @@ pub use hierarchy::*; mod maybe; pub use maybe::*; + +mod template; +pub use template::*; diff --git a/src/template.rs b/src/template.rs new file mode 100644 index 0000000..b93cdab --- /dev/null +++ b/src/template.rs @@ -0,0 +1,358 @@ +//! This is a tiny declarative template library for bevy! +//! +//! The goal is simply to reduce the boilerplate of creating and updating +//! entities. To that end, this crate provides a handy `template!` macro for +//! describing dynamic ECS structres, as well as a simple `Template` struct for +//! holding templates as value. Templates can be built using `Commands::build`, +//! and they automatically update themselves if built multiple times. +//! +//! See the [`template`] macro docs for details. +//! +//! # Compatability +//! +//! This module should not be mixed with the hierarchy module. Use one or the other, not +//! both. +//! +//! # Disclamer +//! +//! This is a first attempt, and was written in about 48 hours over a weekend. There are +//! warts and footguns, issues and bugs. Someone more diligent or more knowlageable about +//! rust macros could probably significantly improve upon this; and if that sounds at all +//! like you I encurage you to try. +//! +//! The `template` macro is implemented declaratively instead of procedurally for no other +//! reason except that I am lazy and it was easier. A proc macro would probably be a better +//! choice. + +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; + +use bevy_hierarchy::prelude::*; +use bevy_ecs::{ + prelude::*, + component::ComponentId +}; + +/// A template is an ordered collection of herogenous prototypes, which can be inserted +/// into the world. +pub type Template = Vec>; + +trait BuildTemplate { + /// Builds a template onto the world. The prototypes in the template are uniquely + /// identified by name. The first time a name appears in a template, a new entity will + /// be spawned. Rebuilding a template with a prototype for that name will update the + /// existing entity instead of creating a new one. + /// + /// Building a template never despawns root level entities (that's your job), but will + /// despawn children of template roots if they fail to match the template. + fn build(self, world: &mut World); +} + +impl BuildTemplate for Template { + fn build(self, world: &mut World) { + world.init_resource::(); + world.resource_scope(|world, mut root: Mut| { + for prototype in self.into_iter() { + let root_receipt = root + .receipts + .entry(prototype.name().to_string()) + .or_default(); + prototype.build(world, root_receipt); + } + }); + } +} + +pub trait WorldTemplateExt { + /// Builds a template. See [`BuildTemplate::build`] for more documentation. + fn build(&mut self, template: Template); +} + +impl WorldTemplateExt for World { + fn build(&mut self, template: Template) { + template.build(self) + } +} + +/// A command for building a template. Created by [`CommandsTemplateExt::build`]. +/// See [`BuildTemplate::build`] for more documentation. +pub struct BuildTemplateCommand(Template); + +impl Command for BuildTemplateCommand { + fn apply(self, world: &mut World) { + self.0.build(world) + } +} + +pub trait CommandsTemplateExt { + /// Builds a template. See [`BuildTemplate::build`] for more documentation. + fn build(&mut self, template: Template); +} + +impl<'w, 's> CommandsTemplateExt for Commands<'w, 's> { + fn build(&mut self, template: Template) { + self.queue(BuildTemplateCommand(template)); + } +} + +/// A prototype is the type-errased trait form of a `Fragment`. It has a name, and can be +/// inserted into the world multiple times, updating it's previous value each time. +/// +/// This trait is mostly needed to get around `Bundle` not being dyn compatable. +pub trait Prototype { + /// Returns the name of this prototype. + fn name(&self) -> Cow<'static, str>; + + /// Builds the prototype on a specific entity. + /// + /// To build a prototype: + /// + /// The prototype uses a receipt to keep track of the state it left the world in when + /// it was last built. The first time it is built, it should use the default receipt. + /// The next time it is built, you should pass the same receipt back in. + fn build(self: Box, world: &mut World, receipt: &mut Receipt); +} + +/// Receipts contain hints about the previous outcome of building a particular prototype. +#[derive(Default)] +pub struct Receipt { + /// The entity this prototype was last built on (if any). + target: Option, + /// The coponents it inserted. + components: HashSet, + /// The receipts of all the children, organized by name. + children: HashMap, +} + +/// A resource that tracks the receipts for root-level templates. +#[derive(Resource, Default)] +pub struct RootReceipt { + receipts: HashMap, +} + +/// A fragment represents a hierarchy of bundles ready to be inserted into the ecs. You can +/// think of it as a named bundle, with other named bundles as children. +pub struct Fragment { + /// The name of the fragment, used to identify children across builds. + pub name: Cow<'static, str>, + /// The bundle to be inserted on the entity. + pub bundle: B, + /// The template for the children. + pub children: Template, +} + +impl Prototype for Fragment { + fn name(&self) -> Cow<'static, str> { + self.name.clone() + } + + fn build(self: Box, world: &mut World, receipt: &mut Receipt) { + // Collect the set of components in the bundle + let mut components = HashSet::new(); + B::get_component_ids(world.components(), &mut |maybe_id| { + if let Some(id) = maybe_id { + components.insert(id); + } + }); + + // Get or spawn the entity + let mut entity = match receipt.target.and_then(|e| world.get_entity_mut(e).ok()) { + Some(entity) => entity, + None => world.spawn_empty(), + }; + let entity_id = entity.id(); + receipt.target = Some(entity_id); + + // Insert the bundle + entity.insert(self.bundle); + + // Remove the components in the previous bundle but not this one + for component_id in receipt.components.difference(&components) { + entity.remove_by_id(*component_id); + } + + // Build the children + let num_children = self.children.len(); + let mut children = Vec::with_capacity(num_children); + let mut child_receipts = HashMap::with_capacity(num_children); + for child in self.children { + let child_name = child.name(); + + // Get or create receipt + let mut child_receipt = receipt + .children + .remove(child_name.as_ref()) + .unwrap_or_default(); + + // Build the child + child.build(world, &mut child_receipt); + + // Return the receipts + children.push(child_receipt.target.unwrap()); + child_receipts.insert(child_name.to_string(), child_receipt); + } + + // Position the children beneith the entity + world.entity_mut(entity_id).replace_children(&children); + + // Clear any remaining orphans + for receipt in receipt.children.values() { + if let Some(entity) = receipt.target { + world.entity_mut(entity).despawn_recursive(); + } + } + + // Update the receipt for use next frame + receipt.components = components; + receipt.children = child_receipts; + } +} + +/// We implement this so that it is easy to return manually constructed a `Fragment` +/// from a block in the `template!` macro. +impl IntoIterator for Fragment { + type Item = Box; + type IntoIter = core::iter::Once; + + fn into_iter(self) -> Self::IntoIter { + std::iter::once(Box::new(self) as Box<_>) + } +} + +/// # Purpose +/// +/// This macro gives you something a little like `jsx`. Much like jsx lets you build and compose html fragments +/// at runtime using normal javascript functions, this lets you build and compose ECS hierarchy fragments +/// in normal rust functions, using normal rust syntax. +/// +/// Here's an example of what it looks like: +/// ```rust +/// # use i_cant_believe_its_not_bsn::*; +/// # use bevy::prelude::*; +/// # let dark_mode = false; +/// # #[derive(Component)] +/// # pub struct MyMarkerComponent; +/// let template = template! { +/// root: {( +/// Text::new(""), +/// TextFont::from_font_size(28.0), +/// if dark_mode { TextColor::WHITE } else { TextColor::BLACK } +/// )} [ +/// hello: { TextSpan::new("Hello") }; +/// world: { TextSpan::new("World") }; +/// punctuation: {( TextSpan::new("!"), MyMarkerComponent )}; +/// ]; +/// }; +/// ``` +/// +/// The grammer is simple: The template contains a list of nodes, each with a name. Each node +/// may also have mote named nodes as children. +/// +/// There is no custom syntax for logic. Every time you see `{ ... }` it's a normal rust code-block, and +/// there are several places where you can substitute in code-blocked for fixed values. +/// +/// The general format of a node is this: +/// 1. The name (eg. `root:`) which may be a fixed symbol or a code-block, and which ends in a colon. +/// 2. A code-block which returns a `Bundle` (eg. `{( TextSpan::new("!"), MyMarkerComponent )}`). +/// 3. Optionally, a list of other nodes in square brackets. +/// +/// You don't have to settle for a static structure either; instead of using the normal node syntax +/// you can just plop in a codeblock which returns `IntoIterator>`. +/// +/// # Composition +/// +/// It's easy to compose functions that return `Templates`. +/// +/// ```rust +/// # use i_cant_believe_its_not_bsn::*; +/// # use bevy::prelude::*; +/// # use bevy::color::palettes::css; +/// fn hello_to(name: String, party_time: bool) -> Template { +/// template! { +/// greetings: { TextSpan::new("Hello") }; +/// name: { +/// if party_time { +/// (TextSpan::new(name), TextColor::default()) +/// } else { +/// (TextSpan::new(format!("{}!!!!!", name)), TextColor(css::HOT_PINK.into())) +/// } +/// }; +/// } +/// } +/// ``` +/// +/// # Useage +/// +/// Once you have a template, you can insert it into the world using `Commands::build`. +/// +/// # Grammer +/// The entire `template!` macro is defined the the following ABNF grammer +/// +/// ```ignore +///