Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,45 @@ the format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)

## unreleased

### changed

- signal graph processed in deterministic topological order instead of the previous nondeterministic depth first order
- `SignalBuilder::*` methods moved to free functions in the `signal` module (e.g. `SignalBuilder::always` -> `signal::always`)
- `JonmoBuilder` renamed to `jonmo::Builder`
- `jonmo::Builder` is now lock-free
- `jonmo::Builder::hold_signals` renamed to `jonmo::Builder::hold_tasks` and takes `Box<dyn SignalTask>`s instead of `SignalHandles`
- renamed `SignalExt::combine` to `SignalExt::zip`
- `MutableVecBuilder`/`MutableBTreeMapBuilder` changed to `MutableVec::builder()`/`MutableBTreeMap::builder()` with simple chainable `.values` and `.with_values` methods
- removed self item `Clone` bound from `SignalVecExt::map_signal` and `SignalVecExt::filter_map`
- `.get_mut`s unwrapped and `.get_entity_mut`s changed to `.entity_mut` in cases where they were silently ignoring invariant violations

### added
- `signal::once`
- `signal::from_component_changed`
- `signal::from_resource_changed`
- `signal::zip!`, a variadic flattened version of `SignalExt::zip`
- `SignalExt::take`
- `SignalVecExt::flatten`
- `track_caller` derive for panicking `LazyEntity` methods
- panic (debug only) or error log that cloning `jonmo::Builder`s at runtime is a bug

### fixed

- deadlock when despawning `MutableVec/BTreeMap`s during another `MutableVec/BTreeMap` despawn
- initially empty `MutableVec/BTreeMap`s work as expected when output to `.switch_signal_vec/map`
- `SignalVecExt::debug` and `SignalMapExt::debug` now log correct code location

### removed
- `*_lazy` signal builder functions, the non-`lazy` versions now take both `Entity` and `LazyEntity`
- `jonmo::Builder::signal_from_*` methods, use corresponding `signal` building functions with `.task()` and `jonmo::Builder::hold_tasks` instead
- `jonmo::Builder::component_signal_from_*` methods, use corresponding `signal` building functions with `jonmo::Builder::component_signal` instead

# 0.5.0 (2025-12-19)

### changed

- `.entity_sync` renamed to `.lazy_entity`
- `SignalExt::combine` always `.clone`s its latest upstream outputs instead of `.take`-ing them
- `SignalExt::combine` emits its latest upstream outputs every frame, unlike previously, when it only emitted on frames where the latest output pair was yet to be emitted

### added

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ documentation = "https://docs.rs/jonmo"
[dependencies]
bevy_app = { version = "0.17", default-features = false }
bevy_derive = { version = "0.17", default-features = false }
bevy_diagnostic = { version = "0.17", default-features = false }
bevy_ecs = { version = "0.17", default-features = false }
bevy_platform = { version = "0.17", default-features = false }
bevy_log = { version = "0.17", default-features = false, optional = true }
Expand All @@ -26,7 +27,7 @@ document-features = { version = "0.2", optional = true }
[features]
default = ["builder", "tracing", "time", "std"]

## Enables access to jonmo's high-level entity builder, [`JonmoBuilder`](https://docs.rs/jonmo/latest/jonmo/builder/struct.JonmoBuilder.html).
## Enables access to jonmo's high-level entity builder, [`jonmo::Builder`](https://docs.rs/jonmo/latest/jonmo/builder/struct.Builder.html).
builder = []

## Enables access to signal ext `.debug` methods, which conveniently logs signal outputs at any step.
Expand Down
25 changes: 11 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
in bengali, jonmo means "birth"
```

[jonmo](https://github.com/databasedav/jonmo) provides an ergonomic, functional, and declarative API for specifying Bevy [system](https://docs.rs/bevy/latest/bevy/ecs/system/index.html) 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](https://docs.rs/jonmo/latest/jonmo/builder/struct.JonmoBuilder.html) which enables one to declare reactive entities, components, and children using a familiar fluent syntax with semantics and API ported from the incredible [FRP](https://en.wikipedia.org/wiki/Functional_reactive_programming) signals of [futures-signals](https://github.com/Pauan/rust-signals) and its web UI dependents [MoonZoon](https://github.com/MoonZoon/MoonZoon) and [Dominator](https://github.com/Pauan/rust-dominator).
[jonmo](https://github.com/databasedav/jonmo) provides an ergonomic, functional, and declarative API for specifying Bevy [system](https://docs.rs/bevy/latest/bevy/ecs/system/index.html) 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](https://docs.rs/jonmo/latest/jonmo/builder/struct.Builder.html) which enables one to declare reactive entities, components, and children using a familiar fluent syntax with semantics and API ported from the incredible [FRP](https://en.wikipedia.org/wiki/Functional_reactive_programming) signals of [futures-signals](https://github.com/Pauan/rust-signals) and its web UI dependents [MoonZoon](https://github.com/MoonZoon/MoonZoon) and [Dominator](https://github.com/Pauan/rust-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`](https://docs.rs/jonmo/latest/jonmo/signal/trait.SignalExt.html), [`SignalVecExt`](https://docs.rs/jonmo/latest/jonmo/signal_vec/trait.SignalVecExt.html), and [`SignalMapExt`](https://docs.rs/jonmo/latest/jonmo/signal_map/trait.SignalMapExt.html) 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.

Expand Down Expand Up @@ -51,18 +51,18 @@ fn main() {
#[derive(Component, Clone, Deref, DerefMut)]
struct Counter(i32);

fn ui_root() -> JonmoBuilder {
fn ui_root() -> jonmo::Builder {
let counter_holder = LazyEntity::new();

JonmoBuilder::from(Node {
jonmo::Builder::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 {
jonmo::Builder::from(Node {
flex_direction: FlexDirection::Row,
column_gap: Val::Px(15.0),
align_items: AlignItems::Center,
Expand All @@ -73,11 +73,10 @@ fn ui_root() -> JonmoBuilder {
.lazy_entity(counter_holder.clone())
.child(counter_button(counter_holder.clone(), PINK, "-", -1))
.child(
JonmoBuilder::from((Node::default(), TextFont::from_font_size(25.)))
jonmo::Builder::from((Node::default(), TextFont::from_font_size(25.)))
.component_signal(
SignalBuilder::from_component_lazy(counter_holder.clone())
.map_in(|counter: Counter| *counter)
.dedupe()
signal::from_component_changed::<Counter>(counter_holder.clone())
.map_in(deref_copied)
.map_in_ref(ToString::to_string)
.map_in(Text)
.map_in(Some),
Expand All @@ -87,8 +86,8 @@ fn ui_root() -> JonmoBuilder {
)
}

fn counter_button(counter_holder: LazyEntity, color: Color, label: &'static str, step: i32) -> JonmoBuilder {
JonmoBuilder::from((
fn counter_button(counter_holder: LazyEntity, color: Color, label: &'static str, step: i32) -> jonmo::Builder {
jonmo::Builder::from((
Node {
width: Val::Px(45.0),
justify_content: JustifyContent::Center,
Expand All @@ -99,11 +98,9 @@ fn counter_button(counter_holder: LazyEntity, color: Color, label: &'static str,
BackgroundColor(color),
))
.observe(move |_: On<Pointer<Click>>, mut counters: Query<&mut Counter>| {
if let Ok(mut counter) = counters.get_mut(*counter_holder) {
**counter += step;
}
**counters.get_mut(*counter_holder).unwrap() += step;
})
.child(JonmoBuilder::from((Text::from(label), TextFont::from_font_size(25.))))
.child(jonmo::Builder::from((Text::from(label), TextFont::from_font_size(25.))))
}

fn camera(mut commands: Commands) {
Expand Down
5 changes: 5 additions & 0 deletions agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Agent Instructions

## Testing

Use `just test` for running tests.
3 changes: 1 addition & 2 deletions examples/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ fn ui(world: &mut World) {
let text = world
.spawn((Node::default(), TextFont::from_font_size(100.), Value(0)))
.id();
let signal = SignalBuilder::from_component(text)
.dedupe()
let signal = signal::from_component_changed::<Value>(text)
.map(move |In(value): In<Value>, mut commands: Commands| {
commands.entity(text).insert(Text(value.0.to_string()));
})
Expand Down
27 changes: 15 additions & 12 deletions examples/basic_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,30 @@ fn main() {
#[derive(Resource, Deref, DerefMut)]
struct ValueTicker(Timer);

#[derive(Component, Clone, Default, PartialEq)]
#[derive(Component, Clone, Default, PartialEq, Deref)]
struct Value(i32);

fn ui() -> JonmoBuilder {
JonmoBuilder::from(Node {
fn ui() -> jonmo::Builder {
jonmo::Builder::from(Node {
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
height: Val::Percent(100.),
width: Val::Percent(100.),
..default()
})
.child(
JonmoBuilder::from((Node::default(), TextFont::from_font_size(100.)))
.child({
let lazy_entity = LazyEntity::new();
jonmo::Builder::from((Node::default(), TextFont::from_font_size(100.)))
.lazy_entity(lazy_entity.clone())
.insert(Value(0))
.component_signal_from_component(|signal| {
signal
.dedupe()
.map(|In(value): In<Value>| Text(value.0.to_string()))
.map_in(Some)
}),
)
.component_signal(
signal::from_component_changed::<Value>(lazy_entity)
.map_in(deref_copied)
.map_in_ref(ToString::to_string)
.map_in(Text)
.map_in(Some),
)
})
}

fn incr_value(mut ticker: ResMut<ValueTicker>, time: Res<Time>, mut values: Query<&mut Value>) {
Expand Down
20 changes: 11 additions & 9 deletions examples/basic_builder_inject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ fn main() {
#[derive(Resource, Deref, DerefMut)]
struct ValueTicker(Timer);

#[derive(Component, Clone, Default, PartialEq)]
#[derive(Component, Clone, Default, PartialEq, Deref)]
struct Value(i32);

fn ui(world: &mut World) {
Expand All @@ -34,14 +34,16 @@ fn ui(world: &mut World) {
});
ui_root.add_child(text);

JonmoBuilder::new()
.component_signal_from_component(|signal| {
signal
.dedupe()
.map(|In(value): In<Value>| Text(value.0.to_string()))
.map_in(Some)
})
.spawn_on_entity(world, text);
jonmo::Builder::new()
.component_signal(
signal::from_component_changed::<Value>(text)
.map_in(deref_copied)
.map_in_ref(ToString::to_string)
.map_in(Text)
.map_in(Some),
)
.spawn_on_entity(world, text)
.unwrap();
}

fn incr_value(mut ticker: ResMut<ValueTicker>, time: Res<Time>, mut values: Query<&mut Value>) {
Expand Down
59 changes: 25 additions & 34 deletions examples/counter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
//! 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.
//! 2. **Declarative UI with `jonmo::Builder`**: 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.
Expand Down Expand Up @@ -40,57 +40,51 @@ fn main() {
#[derive(Component, Clone, Deref, DerefMut)]
struct Counter(i32);

fn ui_root() -> JonmoBuilder {
// --- The `LazyEntity` Pattern ---
fn ui_root() -> jonmo::Builder {
// 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
// spawned by a `jonmo::Builder` 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 {
jonmo::Builder::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 {
jonmo::Builder::from(Node {
flex_direction: FlexDirection::Row,
column_gap: Val::Px(15.0),
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(25.)),
..default()
})
.insert(Counter(0))
// --- Fulfilling the Promise ---
// `.lazy_entity()` 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.
// `.lazy_entity()` connects the `LazyEntity` to the actual entity that this `jonmo::Builder` spawns. Now
// calling `*counter_holder` from other deferred contexts, e.g. in the bodies of signal systems,
// will return the `Entity` ID of this row node.
.lazy_entity(counter_holder.clone())
.child(counter_button(counter_holder.clone(), PINK, "-", -1))
.child(
JonmoBuilder::from((Node::default(), TextFont::from_font_size(25.)))
// --- Reactivity ---
jonmo::Builder::from((Node::default(), TextFont::from_font_size(25.)))
// `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()
// `signal::from_component_changed` creates a signal that reactively reads a component from an
// entity that doesn't exist yet, identified by our `LazyEntity`, only firing when the component
// changes.
signal::from_component_changed::<Counter>(counter_holder.clone())
// `map_in` is a shorthand for `.map` that takes a regular function instead of a Bevy
// system; this is especially convenient when additional `SystemParam`s aren't necessary.
// `deref_copied` dereferences and copies, extracting the inner `i32` from the `Counter`
// newtype for further transformation.
.map_in(deref_copied)
// `Text` expects a `String` and `.to_string` expects a reference
.map_in_ref(ToString::to_string)
.map_in(Text)
Expand All @@ -103,8 +97,8 @@ fn ui_root() -> JonmoBuilder {
)
}

fn counter_button(counter_holder: LazyEntity, color: Color, label: &'static str, step: i32) -> JonmoBuilder {
JonmoBuilder::from((
fn counter_button(counter_holder: LazyEntity, color: Color, label: &'static str, step: i32) -> jonmo::Builder {
jonmo::Builder::from((
Node {
width: Val::Px(45.0),
justify_content: JustifyContent::Center,
Expand All @@ -118,14 +112,11 @@ fn counter_button(counter_holder: LazyEntity, color: Color, label: &'static str,
.observe(move |_: On<Pointer<Click>>, mut counters: Query<&mut Counter>| {
// 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) {
// --- 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;
}
**counters.get_mut(*counter_holder).unwrap() += step;
// 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.
})
.child(JonmoBuilder::from((Text::from(label), TextFont::from_font_size(25.))))
.child(jonmo::Builder::from((Text::from(label), TextFont::from_font_size(25.))))
}

fn camera(mut commands: Commands) {
Expand Down
Loading