2 releases
new 0.1.1 | Jan 16, 2025 |
---|---|
0.1.0 | Jan 15, 2025 |
#463 in Game dev
184 downloads per month
92KB
1K
SLoC
recompose
recompose
is a crate for Bevy that provides a declarative way to compose structures in
a way that is easy to write, and is ECS- and borrow-checker friendly.
It is most useful for UI-building, but can be applied to other ECS-structures as well. For more information, check out the examples and docs.
Example
use bevy::prelude::*;
use recompose::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(RecomposePlugin)
.add_systems(Startup, setup)
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
commands.spawn((Root::new(squares), Node::default()));
}
// `Fn(&mut Scope) -> impl Compose` implements Compose, so we can use functions for simple composables.
fn squares(cx: &mut Scope) -> impl Compose {
let count = cx.use_state(42);
Node {
display: Display::Flex,
column_gap: Val::Px(8.0),
..default()
}
.children((
Square(Srgba::RED.into()),
Square(Srgba::GREEN.into()),
Square(Srgba::BLUE.into()),
Text::new(count.to_string()).to_compose(),
))
}
#[derive(Clone)]
struct Square(Color);
// For more complex composables with input, we can implement Compose directly on a struct.
impl Compose for Square {
fn compose<'a>(&self, _: &mut Scope) -> impl Compose + 'a {
(
Node {
width: Val::Px(32.0),
height: Val::Px(32.0),
..default()
},
BackgroundColor(self.0),
)
.to_compose()
}
}
Hooks and composing
If you've ever used React or similar libraries, you might be familiar with the concept of "hooks". In recompose
,
"hooks" are functions that interact and/or modify the Scope
. Read the How it works section for
more details.
Unlike React, not all hooks are required to follow the "rules of hooks" - being called in the same order, and never
conditionally, in a loop and so on. Functions that must obey these rules are prefixed wiht use_
.
Some of the hooks available:
State
let count = cx.use_state(42); // Get a state that persists between recompositions.
cx.set_state(&count, *count + 1); // Set the state to a new value.
Composable lifetime
cx.use_mount(|| { /* Do something */}); // Called when the composable is first composed.
cx.effect(|| { /* Do something */}, (&count, &name)); // Called only when dependencies have changed.
ECS World interaction
// Run a system each time the composable is recomposed.
cx.run_system(|names: Query<&Name>, mut state: SetState| {
state.set(&count, 30); // Set the state to a new value.
state.modify(&count, |count| *count + 1); // Modify the state.
});
// Run a system once, when the composable is first composed.
cx.use_system_once(|| { /* .. */ });
How it works
recompose
is built around the concept of composables that implement the Compose
trait. The
compose
function of the Compose
trait modifes the given Scope
(a Scope
can be thought of as a node in a tree-structure) and may return a new Compose
, which is then added as
the current Scope
s' child. The compose function is called when the composable is first added, when one of the
scope's state changes, or when the parent composable "recomposes".
Some notable Compose
implementations:
Fn(&mut Scope) -> impl Compose
- A function that takes a mutable reference to aScope
and returns a composable. Useful for simple composables that don't need any input.Spawn
- Spawns a new entity with the from a bundle.DynCompose
- Allows for dynamic composables that "erase" their type definition.Option<C>
- ComposesC
if the option isSome
, otherwise does nothing.- Tuples
(C0, .., C9)
- Compose multiple composables at once. Vec<C>
- Compose any number of composables. This requires that the items implement theKey
-trait.Keyed
- Implements theKey
-trait and can be used to wrap any composable. The added advantage is that the type is "erased" so that composables of different types can be composed in the sameVec
.
()
- Empty composable that does nothing. ImplementingCompose
for()
lets us skip returning anything from thecompose
function.
Bundle
- The
Bundle
-trait does not implemented theCompose
-trait, however it does implement theBundleExtension
-trait which lets us convert aBundle
into aSpawn
-composable very easily. ThroughBundleExtension
, it also implementsModfiyFunctions
-trait which lets us use functions likechildren
,observe
.
Compatibility with Bevy
Bevy | recompose |
---|---|
0.15 | 0.1 |
Motivation
Recompose is heavily inspired by the actuate crate, which also provides a declarative
way to construct ECS structures. This crate strives to be more robust, safer and less error-prone than actuate
.
The goal of recompose
is not necessarily to be the most performant solution, but rather one that is easy to use
and easy to understand.
Dependencies
~34–65MB
~1M SLoC