Expand description
§jonmo জন্ম
in bengali, jonmo means "birth"
jonmo provides an ergonomic, functional, and declarative API for specifying Bevy system dependency graphs, where “output” handles to nodes of the graph are canonically referred to as “signals”. Building upon these signals, jonmo offers a high level entity builder which enables one to declare reactive entities, components, and children using a familiar fluent syntax with semantics and API ported from the incredible FRP signals of futures-signals and its web UI dependents MoonZoon and Dominator.
The runtime of jonmo is quite simple; every frame, the outputs of systems are forwarded to their dependants, recursively. The complexity and power of jonmo really emerges from its monadic signal combinators, defined within the SignalExt
, SignalVecExt
, and SignalMapExt
traits (ported from futures-signals’ traits of the same name), which internally manage special Bevy systems that allow for the declarative composition of complex data flows with minimalistic, high-level, signals-oriented methods.
§assorted features:
- fine-grained reactivity for all entities, components, and children
- diff-less constant-time reactive updates for collections (available through
MutableVec
andMutableBTreeMap
) - automated system lifecycle management when using the builder API, simple component on-remove hook when not
- polling API for when one needs an escape hatch from the regular push-based output semantics (polling is used sparsely internally for some combinators)
- either wrappers (a la https://github.com/rayon-rs/either) and type-erased signals (via boxing) for cheap and flexible management of distinct signal types from different branches of logic
no_std
compatible
§feature flags
§examples
//! Simple counter example, ported from a similar example in haalka.
//!
//! This example introduces some concepts for building reactive Bevy applications using jonmo's
//! signals:
//!
//! 1. **World-Driven State**: The application's state (the counter's value) is stored in a standard
//! Bevy `Component`, the "single source of truth".
//!
//! 2. **Declarative UI with `JonmoBuilder`**: The entire entity hierarchy is defined up-front in a
//! clean, colocated, declarative style.
//!
//! 3. **The `LazyEntity` Pattern**: related entities can refer to the state-holding entity *before*
//! it has been spawned, solving a common ordering problem in hierarchical entity construction.
//!
//! 4. **Reactivity with `.signal` methods**: A signal is created that reads the state component,
//! whose output is then used to reactively update other components, like the `Text` for the
//! counter's display.
use bevy::prelude::*;
use jonmo::prelude::*;
fn main() {
App::new()
.add_plugins((DefaultPlugins, JonmoPlugin))
.add_systems(
Startup,
(
|world: &mut World| {
ui_root().spawn(world);
},
camera,
),
)
.run();
}
#[derive(Component, Clone, Deref, DerefMut)]
struct Counter(i32);
fn ui_root() -> JonmoBuilder {
// --- The `LazyEntity` Pattern ---
// We need the buttons and the text display to know the `Entity` ID of the node
// that will hold the `Counter` component. However, that node hasn't been spawned yet.
// `LazyEntity` acts as a placeholder or a "promise" for an entity that will be
// spawned by a `JonmoBuilder` later. It can be cloned and passed around freely, e.g. into the
// bodies of signal systems.
let counter_holder = LazyEntity::new();
JonmoBuilder::from(Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
})
.child(
JonmoBuilder::from(Node {
flex_direction: FlexDirection::Row,
column_gap: Val::Px(15.0),
align_items: AlignItems::Center,
..default()
})
.insert(Counter(0))
// --- Fulfilling the Promise ---
// `.entity_sync()` connects the `LazyEntity` to the actual entity that this `JonmoBuilder` spawns. Now calling
// `counter_holder.get()` from other deferred contexts, e.g. in the bodies of signal systems, will return the
// `Entity` ID of this row node.
.entity_sync(counter_holder.clone())
.child(counter_button(counter_holder.clone(), PINK, "-", -1))
.child(
JonmoBuilder::from((Node::default(), TextFont::from_font_size(25.)))
// --- Reactivity ---
// `component_signal` is a core fixture of jonmo's reactivity. It takes a signal as an argument and
// uses its output to insert or update a component on the entity being built. Here,
// we're creating a signal that will produce a `Text` component whenever the counter
// changes.
.component_signal(
// `SignalBuilder::from_component_lazy` creates a signal that reactively reads a component from an
// entity that doesn't exist yet, identified by our `LazyEntity`.
SignalBuilder::from_component_lazy(counter_holder.clone())
// Rust's type system is intelligent enough to infer the "from_component" target from the
// definition of the system passed to any of jonmo's signal combinators. `map_in` is a shorthand
// for `.map` which unpacks its `In` input; this is especially convenient when the system passed
// to `.map` does not need any other `SystemParam`s. Here, we simply deref the inner counter
// value from the fetched `Counter` component for further transformation.
.map_in(|counter: Counter| *counter)
// `.dedupe()` ensures the rest of the chain only runs when the counter's value *actually
// changes*, preventing redundant updates every frame.
.dedupe()
// `Text` expects a `String`
.map_in_ref(ToString::to_string)
.map_in(Text)
// `component_signal` expects an `Option<Component>`. If the signal produces `None`, the
// component is removed. Here, our value is always present, so we wrap it in `Some`.
.map_in(Some),
),
)
.child(counter_button(counter_holder, BLUE, "+", 1)),
)
}
fn counter_button(counter_holder: LazyEntity, color: Color, label: &'static str, step: i32) -> JonmoBuilder {
JonmoBuilder::from((
Node {
width: Val::Px(45.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BorderRadius::MAX,
BackgroundColor(color),
))
// `.with_entity()` provides a convenient hook to run code on this builder's `EntityWorldMut`, e.g. for setting up
// observers
.with_entity(move |mut entity| {
entity.observe(
move |trigger: Trigger<Pointer<Click>>, mut counters: Query<&mut Counter>| {
if matches!(trigger.button, PointerButton::Primary) {
// Use the fulfilled `LazyEntity` to get mutable access to the `Counter` component on our
// state-holding entity.
if let Ok(mut counter) = counters.get_mut(counter_holder.get()) {
// --- State Mutation ---
// Because our text display has a signal that reads this component, this change will
// automatically trigger a UI update at the end of the frame.
**counter += step;
}
}
},
);
})
.child(JonmoBuilder::from((Text::from(label), TextFont::from_font_size(25.))))
}
fn camera(mut commands: Commands) {
commands.spawn(Camera2d);
}
§on the web
All examples are compiled to wasm for both webgl2 and webgpu (check compatibility) and deployed to github pages.
-
a simple increasing timer without using the entity builder, showcasing the least invasive way to start using jonmo signals in existing Bevy apps
-
basic_builder_inject
webgl2 webgpua simple increasing timer injecting the entity builder into an existing entity, showcasing a less invasive way to start using jonmo signals in existing Bevy apps.
-
a simple increasing timer using the entity builder, showcasing the recommended, idiomatic way to use jonmo signals
-
the example above, a simple counter
-
a reactive enumerated list of colors, each with an independent lifetime timer
-
simple key press counter, showcasing map reactivity
-
diverse filtering options for a list of items, showcasing vector reactivity
§Bevy compatibility
bevy | jonmo |
---|---|
0.16 | 0.2 |
0.15 | 0.1 |
§license
All code in this repository is dual-licensed under either:
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
at your option.
§your contributions
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
§feature flags
builder
(enabled by default) — Enables access to jonmo’s high-level entity builder,JonmoBuilder
.tracing
(enabled by default) — Enables access to signal ext.debug
methods, which conveniently logs signal outputs at any step.time
(enabled by default) — Enables access toSignalExt::throttle
, which delays subsequent outputs by someDuration
.debug
—DebugUiPlugin
which enables togglingbevy_dev_tools::ui_debug_overlay::DebugUiPlugin
with theF1
key; requires a camera to be marked with theIsDefaultCamera
component.std
(enabled by default) — Uses the Rust standard library.critical-section
— Rely oncritical-section
for synchronization primitives.web
— Enables access to browser APIs in a web context.
Modules§
- builder
- Reactive entity builder ported from Dominator’s
DomBuilder
. - graph
- Signal graph management and runtime.
- prelude
use jonmo::prelude::*;
imports everything one needs to use start using jonmo.- signal
- Signal builders and combinators for constructing reactive
System
dependency graphs, seeSignalExt
. - signal_
map - Data structures and combinators for constructing reactive
System
dependency graphs on top ofBTreeMap
mutations, seeMutableBTreeMap
andSignalMapExt
. - signal_
vec - Data structures and combinators for constructing reactive
System
dependency graphs on top ofVec
mutations, seeMutableVec
andSignalVecExt
. - utils
Structs§
- Jonmo
Plugin - Includes the systems required for jonmo to function.