Skip to content

Commit ccd1af5

Browse files
authored
more changes for haalka bevy 0.17 (#28)
1 parent d5de884 commit ccd1af5

21 files changed

Lines changed: 7358 additions & 4370 deletions

CHANGELOG.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,46 @@ the format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
44

55
## unreleased
66

7+
### changed
8+
9+
- signal graph processed in deterministic topological order instead of the previous nondeterministic depth first order
10+
- `SignalBuilder::*` methods moved to free functions in the `signal` module (e.g. `SignalBuilder::always` -> `signal::always`)
11+
- `JonmoBuilder` renamed to `jonmo::Builder`
12+
- `jonmo::Builder` is now lock-free
13+
- `jonmo::Builder::hold_signals` renamed to `jonmo::Builder::hold_tasks` and takes `Box<dyn SignalTask>`s instead of `SignalHandles`
14+
- renamed `SignalExt::combine` to `SignalExt::zip`
15+
- `MutableVecBuilder`/`MutableBTreeMapBuilder` changed to `MutableVec::builder()`/`MutableBTreeMap::builder()` with simple chainable `.values` and `.with_values` methods
16+
- removed self item `Clone` bound from `SignalVecExt::map_signal` and `SignalVecExt::filter_map`
17+
- `.get_mut`s unwrapped and `.get_entity_mut`s changed to `.entity_mut` in cases where they were silently ignoring invariant violations
18+
19+
### added
20+
- `SignalExt/SignalVecExt/SignalMapExt::schedule`, enabling granular control of which schedule each node in the signal graph runs during
21+
- `SignalExt::take`
22+
- `SignalExt::skip`
23+
- `signal::once`
24+
- `signal::from_component_changed`
25+
- `signal::from_resource_changed`
26+
- `signal::zip!`, a variadic flattened version of `SignalExt::zip`
27+
- `track_caller` derive for panicking `LazyEntity` methods
28+
- panic (debug only) or error log that cloning `jonmo::Builder`s at runtime is a bug
29+
30+
### fixed
31+
32+
- deadlock when despawning `MutableVec/BTreeMap`s during another `MutableVec/BTreeMap` despawn
33+
- initially empty `MutableVec/BTreeMap`s work as expected when output to `.switch_signal_vec/map`
34+
- `SignalVecExt::debug` and `SignalMapExt::debug` now log correct code location
35+
36+
### removed
37+
- `*_lazy` signal builder functions, the non-`lazy` versions now take both `Entity` and `LazyEntity`
38+
- `jonmo::Builder::signal_from_*` methods, use corresponding `signal` building functions with `.task()` and `jonmo::Builder::hold_tasks` instead
39+
- `jonmo::Builder::component_signal_from_*` methods, use corresponding `signal` building functions with `jonmo::Builder::component_signal` instead
40+
741
# 0.5.0 (2025-12-19)
842

943
### changed
1044

1145
- `.entity_sync` renamed to `.lazy_entity`
12-
- `SignalExt::combine` always `.clone`s its latest upstream outputs instead of `.take`-ing them
46+
- `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
1347

1448
### added
1549

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ documentation = "https://docs.rs/jonmo"
1313
[dependencies]
1414
bevy_app = { version = "0.17", default-features = false }
1515
bevy_derive = { version = "0.17", default-features = false }
16+
bevy_diagnostic = { version = "0.17", default-features = false }
1617
bevy_ecs = { version = "0.17", default-features = false }
1718
bevy_platform = { version = "0.17", default-features = false }
1819
bevy_log = { version = "0.17", default-features = false, optional = true }
@@ -26,7 +27,7 @@ document-features = { version = "0.2", optional = true }
2627
[features]
2728
default = ["builder", "tracing", "time", "std"]
2829

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

3233
## Enables access to signal ext `.debug` methods, which conveniently logs signal outputs at any step.

README.md

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
in bengali, jonmo means "birth"
99
```
1010

11-
[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).
11+
[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).
1212

1313
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.
1414

@@ -51,18 +51,18 @@ fn main() {
5151
#[derive(Component, Clone, Deref, DerefMut)]
5252
struct Counter(i32);
5353
54-
fn ui_root() -> JonmoBuilder {
54+
fn ui_root() -> jonmo::Builder {
5555
let counter_holder = LazyEntity::new();
5656
57-
JonmoBuilder::from(Node {
57+
jonmo::Builder::from(Node {
5858
width: Val::Percent(100.0),
5959
height: Val::Percent(100.0),
6060
justify_content: JustifyContent::Center,
6161
align_items: AlignItems::Center,
6262
..default()
6363
})
6464
.child(
65-
JonmoBuilder::from(Node {
65+
jonmo::Builder::from(Node {
6666
flex_direction: FlexDirection::Row,
6767
column_gap: Val::Px(15.0),
6868
align_items: AlignItems::Center,
@@ -73,11 +73,10 @@ fn ui_root() -> JonmoBuilder {
7373
.lazy_entity(counter_holder.clone())
7474
.child(counter_button(counter_holder.clone(), PINK, "-", -1))
7575
.child(
76-
JonmoBuilder::from((Node::default(), TextFont::from_font_size(25.)))
76+
jonmo::Builder::from((Node::default(), TextFont::from_font_size(25.)))
7777
.component_signal(
78-
SignalBuilder::from_component_lazy(counter_holder.clone())
79-
.map_in(|counter: Counter| *counter)
80-
.dedupe()
78+
signal::from_component_changed::<Counter>(counter_holder.clone())
79+
.map_in(deref_copied)
8180
.map_in_ref(ToString::to_string)
8281
.map_in(Text)
8382
.map_in(Some),
@@ -87,8 +86,8 @@ fn ui_root() -> JonmoBuilder {
8786
)
8887
}
8988
90-
fn counter_button(counter_holder: LazyEntity, color: Color, label: &'static str, step: i32) -> JonmoBuilder {
91-
JonmoBuilder::from((
89+
fn counter_button(counter_holder: LazyEntity, color: Color, label: &'static str, step: i32) -> jonmo::Builder {
90+
jonmo::Builder::from((
9291
Node {
9392
width: Val::Px(45.0),
9493
justify_content: JustifyContent::Center,
@@ -99,11 +98,9 @@ fn counter_button(counter_holder: LazyEntity, color: Color, label: &'static str,
9998
BackgroundColor(color),
10099
))
101100
.observe(move |_: On<Pointer<Click>>, mut counters: Query<&mut Counter>| {
102-
if let Ok(mut counter) = counters.get_mut(*counter_holder) {
103-
**counter += step;
104-
}
101+
**counters.get_mut(*counter_holder).unwrap() += step;
105102
})
106-
.child(JonmoBuilder::from((Text::from(label), TextFont::from_font_size(25.))))
103+
.child(jonmo::Builder::from((Text::from(label), TextFont::from_font_size(25.))))
107104
}
108105
109106
fn camera(mut commands: Commands) {

examples/basic.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ fn ui(world: &mut World) {
2626
let text = world
2727
.spawn((Node::default(), TextFont::from_font_size(100.), Value(0)))
2828
.id();
29-
let signal = SignalBuilder::from_component(text)
30-
.dedupe()
29+
let signal = signal::from_component_changed::<Value>(text)
3130
.map(move |In(value): In<Value>, mut commands: Commands| {
3231
commands.entity(text).insert(Text(value.0.to_string()));
3332
})

examples/basic_builder.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,27 +26,30 @@ fn main() {
2626
#[derive(Resource, Deref, DerefMut)]
2727
struct ValueTicker(Timer);
2828

29-
#[derive(Component, Clone, Default, PartialEq)]
29+
#[derive(Component, Clone, Default, PartialEq, Deref)]
3030
struct Value(i32);
3131

32-
fn ui() -> JonmoBuilder {
33-
JonmoBuilder::from(Node {
32+
fn ui() -> jonmo::Builder {
33+
jonmo::Builder::from(Node {
3434
justify_content: JustifyContent::Center,
3535
align_items: AlignItems::Center,
3636
height: Val::Percent(100.),
3737
width: Val::Percent(100.),
3838
..default()
3939
})
40-
.child(
41-
JonmoBuilder::from((Node::default(), TextFont::from_font_size(100.)))
40+
.child({
41+
let lazy_entity = LazyEntity::new();
42+
jonmo::Builder::from((Node::default(), TextFont::from_font_size(100.)))
43+
.lazy_entity(lazy_entity.clone())
4244
.insert(Value(0))
43-
.component_signal_from_component(|signal| {
44-
signal
45-
.dedupe()
46-
.map(|In(value): In<Value>| Text(value.0.to_string()))
47-
.map_in(Some)
48-
}),
49-
)
45+
.component_signal(
46+
signal::from_component_changed::<Value>(lazy_entity)
47+
.map_in(deref_copied)
48+
.map_in_ref(ToString::to_string)
49+
.map_in(Text)
50+
.map_in(Some),
51+
)
52+
})
5053
}
5154

5255
fn incr_value(mut ticker: ResMut<ValueTicker>, time: Res<Time>, mut values: Query<&mut Value>) {

examples/basic_builder_inject.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ fn main() {
1818
#[derive(Resource, Deref, DerefMut)]
1919
struct ValueTicker(Timer);
2020

21-
#[derive(Component, Clone, Default, PartialEq)]
21+
#[derive(Component, Clone, Default, PartialEq, Deref)]
2222
struct Value(i32);
2323

2424
fn ui(world: &mut World) {
@@ -34,14 +34,16 @@ fn ui(world: &mut World) {
3434
});
3535
ui_root.add_child(text);
3636

37-
JonmoBuilder::new()
38-
.component_signal_from_component(|signal| {
39-
signal
40-
.dedupe()
41-
.map(|In(value): In<Value>| Text(value.0.to_string()))
42-
.map_in(Some)
43-
})
44-
.spawn_on_entity(world, text);
37+
jonmo::Builder::new()
38+
.component_signal(
39+
signal::from_component_changed::<Value>(text)
40+
.map_in(deref_copied)
41+
.map_in_ref(ToString::to_string)
42+
.map_in(Text)
43+
.map_in(Some),
44+
)
45+
.spawn_on_entity(world, text)
46+
.unwrap();
4547
}
4648

4749
fn incr_value(mut ticker: ResMut<ValueTicker>, time: Res<Time>, mut values: Query<&mut Value>) {

examples/counter.rs

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
//! 1. **World-Driven State**: The application's state (the counter's value) is stored in a standard
77
//! Bevy `Component`, the "single source of truth".
88
//!
9-
//! 2. **Declarative UI with `JonmoBuilder`**: The entire entity hierarchy is defined up-front in a
10-
//! clean, colocated, declarative style.
9+
//! 2. **Declarative UI with `jonmo::Builder`**: The entire entity hierarchy is defined up-front in
10+
//! a clean, colocated, declarative style.
1111
//!
1212
//! 3. **The `LazyEntity` Pattern**: related entities can refer to the state-holding entity *before*
1313
//! it has been spawned, solving a common ordering problem in hierarchical entity construction.
@@ -40,57 +40,51 @@ fn main() {
4040
#[derive(Component, Clone, Deref, DerefMut)]
4141
struct Counter(i32);
4242

43-
fn ui_root() -> JonmoBuilder {
44-
// --- The `LazyEntity` Pattern ---
43+
fn ui_root() -> jonmo::Builder {
4544
// We need the buttons and the text display to know the `Entity` ID of the node
4645
// that will hold the `Counter` component. However, that node hasn't been spawned yet.
4746
// `LazyEntity` acts as a placeholder or a "promise" for an entity that will be
48-
// spawned by a `JonmoBuilder` later. It can be cloned and passed around freely, e.g. into the
47+
// spawned by a `jonmo::Builder` later. It can be cloned and passed around freely, e.g. into the
4948
// bodies of signal systems.
5049
let counter_holder = LazyEntity::new();
5150

52-
JonmoBuilder::from(Node {
51+
jonmo::Builder::from(Node {
5352
width: Val::Percent(100.0),
5453
height: Val::Percent(100.0),
5554
justify_content: JustifyContent::Center,
5655
align_items: AlignItems::Center,
5756
..default()
5857
})
5958
.child(
60-
JonmoBuilder::from(Node {
59+
jonmo::Builder::from(Node {
6160
flex_direction: FlexDirection::Row,
6261
column_gap: Val::Px(15.0),
6362
align_items: AlignItems::Center,
6463
padding: UiRect::all(Val::Px(25.)),
6564
..default()
6665
})
6766
.insert(Counter(0))
68-
// --- Fulfilling the Promise ---
69-
// `.lazy_entity()` connects the `LazyEntity` to the actual entity that this `JonmoBuilder` spawns. Now calling
70-
// `counter_holder.get()` from other deferred contexts, e.g. in the bodies of signal systems, will return the
71-
// `Entity` ID of this row node.
67+
// `.lazy_entity()` connects the `LazyEntity` to the actual entity that this `jonmo::Builder` spawns. Now
68+
// calling `*counter_holder` from other deferred contexts, e.g. in the bodies of signal systems,
69+
// will return the `Entity` ID of this row node.
7270
.lazy_entity(counter_holder.clone())
7371
.child(counter_button(counter_holder.clone(), PINK, "-", -1))
7472
.child(
75-
JonmoBuilder::from((Node::default(), TextFont::from_font_size(25.)))
76-
// --- Reactivity ---
73+
jonmo::Builder::from((Node::default(), TextFont::from_font_size(25.)))
7774
// `component_signal` is a core fixture of jonmo's reactivity. It takes a signal as an argument and
7875
// uses its output to insert or update a component on the entity being built. Here,
7976
// we're creating a signal that will produce a `Text` component whenever the counter
8077
// changes.
8178
.component_signal(
82-
// `SignalBuilder::from_component_lazy` creates a signal that reactively reads a component from an
83-
// entity that doesn't exist yet, identified by our `LazyEntity`.
84-
SignalBuilder::from_component_lazy(counter_holder.clone())
85-
// Rust's type system is intelligent enough to infer the "from_component" target from the
86-
// definition of the system passed to any of jonmo's signal combinators. `map_in` is a shorthand
87-
// for `.map` which unpacks its `In` input; this is especially convenient when the system passed
88-
// to `.map` does not need any other `SystemParam`s. Here, we simply deref the inner counter
89-
// value from the fetched `Counter` component for further transformation.
90-
.map_in(|counter: Counter| *counter)
91-
// `.dedupe()` ensures the rest of the chain only runs when the counter's value *actually
92-
// changes*, preventing redundant updates every frame.
93-
.dedupe()
79+
// `signal::from_component_changed` creates a signal that reactively reads a component from an
80+
// entity that doesn't exist yet, identified by our `LazyEntity`, only firing when the component
81+
// changes.
82+
signal::from_component_changed::<Counter>(counter_holder.clone())
83+
// `map_in` is a shorthand for `.map` that takes a regular function instead of a Bevy
84+
// system; this is especially convenient when additional `SystemParam`s aren't necessary.
85+
// `deref_copied` dereferences and copies, extracting the inner `i32` from the `Counter`
86+
// newtype for further transformation.
87+
.map_in(deref_copied)
9488
// `Text` expects a `String` and `.to_string` expects a reference
9589
.map_in_ref(ToString::to_string)
9690
.map_in(Text)
@@ -103,8 +97,8 @@ fn ui_root() -> JonmoBuilder {
10397
)
10498
}
10599

106-
fn counter_button(counter_holder: LazyEntity, color: Color, label: &'static str, step: i32) -> JonmoBuilder {
107-
JonmoBuilder::from((
100+
fn counter_button(counter_holder: LazyEntity, color: Color, label: &'static str, step: i32) -> jonmo::Builder {
101+
jonmo::Builder::from((
108102
Node {
109103
width: Val::Px(45.0),
110104
justify_content: JustifyContent::Center,
@@ -118,14 +112,11 @@ fn counter_button(counter_holder: LazyEntity, color: Color, label: &'static str,
118112
.observe(move |_: On<Pointer<Click>>, mut counters: Query<&mut Counter>| {
119113
// Use the fulfilled `LazyEntity` to get mutable access to the `Counter` component on our
120114
// state-holding entity.
121-
if let Ok(mut counter) = counters.get_mut(*counter_holder) {
122-
// --- State Mutation ---
123-
// Because our text display has a signal that reads this component, this change will
124-
// automatically trigger a UI update at the end of the frame.
125-
**counter += step;
126-
}
115+
**counters.get_mut(*counter_holder).unwrap() += step;
116+
// Because our text display has a signal that reads this component, this change will
117+
// automatically trigger a UI update at the end of the frame.
127118
})
128-
.child(JonmoBuilder::from((Text::from(label), TextFont::from_font_size(25.))))
119+
.child(jonmo::Builder::from((Text::from(label), TextFont::from_font_size(25.))))
129120
}
130121

131122
fn camera(mut commands: Commands) {

0 commit comments

Comments
 (0)