lifetimes/lifetimes.rs
1//! This example showcases a more advanced, dynamic UI using `jonmo`.
2//! It demonstrates:
3//! - A reactive list of items whose length can change at runtime.
4//! - How each item in the list can have its own internal state and reactive updates.
5//! - Communication from a child UI element (a "remove" button) back to the central data source.
6//! - The use of `LazyEntity` to create signals that depend on an entity that hasn't been spawned
7//! yet.
8//!
9//! The application displays a list of colored bars. Each bar has a "lifetime" counter that
10//! continuously updates. You can add new bars by pressing the `=` key, remove the last bar with
11//! the `-` key, or click the red 'x' button on any bar to remove it specifically.
12//! This pattern is fundamental for building complex, data-driven UIs like settings menus,
13//! inventory screens, or leaderboards.
14
15// This example uses a few helper functions, like `random_color`.
16mod utils;
17use utils::*;
18
19use bevy::prelude::*;
20use jonmo::prelude::*;
21
22fn main() {
23 let mut app = App::new();
24 let world = app.world_mut();
25 // 1. --- DATA SOURCE SETUP ---
26 // `MutableVec` is the core reactive data source for lists in `jonmo`.
27 // We initialize it with two random colors.
28 // It's wrapped in an `Arc<RwLock<...>>` internally, so cloning it is cheap
29 // and allows multiple systems to access and modify the same data.
30 let colors = MutableVecBuilder::from([random_color(), random_color()]).spawn(world);
31
32 app.add_plugins(examples_plugin)
33 // 2. --- RESOURCE MANAGEMENT ---
34 // We insert a clone of our `MutableVec` into a Bevy resource. This makes it
35 // accessible to any system that needs to read or modify the list of colors,
36 // such as our `hotkeys` system or the remove button's `observe` system.
37 .insert_resource(Colors(colors.clone()))
38 .add_systems(
39 // We use `PostStartup` to ensure that Bevy's UI systems are initialized
40 // before we try to spawn our UI.
41 Startup,
42 (
43 // 3. --- UI SPAWNING ---
44 // We move the `colors` `MutableVec` into a closure that will spawn the UI.
45 // `colors.signal_vec()` creates a `SignalVec`, which is a stream of
46 // changes (`VecDiff`s) that other parts of the UI can subscribe to.
47 move |world: &mut World| {
48 ui_root(colors.signal_vec()).spawn(world);
49 },
50 camera,
51 ),
52 )
53 // 4. --- UPDATE SYSTEMS ---
54 // These systems run every frame.
55 .add_systems(
56 Update,
57 (
58 // The `live` system increments the lifetime of each list item.
59 // It only runs if there is at least one entity with a `Lifetime` component.
60 live.run_if(any_with_component::<Lifetime>),
61 ),
62 )
63 .run();
64}
65
66/// A Bevy resource that holds a clone of the `MutableVec` of colors.
67/// This allows different systems to easily access the central data source.
68#[derive(Resource, Clone)]
69struct Colors(MutableVec<Color>);
70
71/// A component to track the "lifetime" of a list item, in seconds.
72/// We'll use this to demonstrate that each item in the reactive list
73/// can have its own independent, stateful logic.
74#[derive(Component, Default, Clone)]
75struct Lifetime(f32);
76
77/// Constructs the root UI node.
78///
79/// It takes a `SignalVec` of `Color`s as input. This is the reactive "pipe"
80/// that will drive the creation, destruction, and updating of child elements.
81fn ui_root(colors: impl SignalVec<Item = Color>) -> JonmoBuilder {
82 // A standard vertical flexbox to hold our list items.
83 JonmoBuilder::from(Node {
84 height: Val::Percent(100.0),
85 width: Val::Percent(100.0),
86 flex_direction: FlexDirection::Column,
87 align_items: AlignItems::Center,
88 justify_content: JustifyContent::Center,
89 row_gap: Val::Px(10.0),
90 ..default()
91 })
92 // This is the core of the dynamic list.
93 // `children_signal_vec` subscribes to a `SignalVec`. For each item in the
94 // vector, it spawns a child entity using the `JonmoBuilder` returned by the closure.
95 // It handles all diffs automatically: `Push` creates a new child, `RemoveAt`
96 // despawns one, `Move` reorders them, etc.
97 .children_signal_vec(
98 // `.enumerate()` is a powerful combinator that transforms a `SignalVec<T>`
99 // into a `SignalVec<(Signal<Option<usize>>, T)>`.
100 // The first element of the tuple is a *new signal* that will always contain
101 // the current index of that specific item, or `None` if it has been removed.
102 // This is crucial for displaying the index or for actions like removing a specific item.
103 colors.enumerate().map_in(|(index, color)| item(index.dedupe(), color)),
104 )
105 .child(
106 JonmoBuilder::from((
107 Node {
108 height: Val::Px(40.),
109 width: Val::Px(100.),
110 align_items: AlignItems::Center,
111 justify_content: JustifyContent::Center,
112 ..default()
113 },
114 BackgroundColor(bevy::color::palettes::basic::GREEN.into()),
115 ))
116 // `on_spawn` runs a closure with access to the `World` and the spawned `Entity`
117 // just after the entity is created. This is a good place to set up observers or
118 // other one-time logic.
119 .on_spawn(|world, entity| {
120 // `observe` is a Bevy event-handling pattern. Here, we're setting up this
121 // button entity to listen for a `Click` event.
122 world.entity_mut(entity).observe(
123 // This closure is the event handler that runs when the button is clicked.
124 move |_: Trigger<Pointer<Click>>,
125 colors: Res<Colors>,
126 mut mutable_vec_datas: Query<&mut MutableVecData<_>>| {
127 // Try to get the `Index` component from the clicked entity.
128 // We found the index! Now we can mutate the central data source.
129 // `colors.0.write()` gets a write lock on the `MutableVec`.
130 let mut guard = colors.0.write(&mut mutable_vec_datas);
131 guard.insert(guard.len(), random_color());
132 },
133 );
134 })
135 .child(JonmoBuilder::from((
136 Node::default(),
137 Text::new("+"),
138 TextColor(Color::WHITE),
139 TextLayout::new_with_justify(JustifyText::Center),
140 ))),
141 )
142}
143
144/// A component to hold the index of a list item. This is inserted onto the
145/// "remove" button so that when it's clicked, we know which item in the
146/// `MutableVec` to remove.
147#[derive(Component, Clone)]
148struct Index(usize);
149
150/// Constructs a `JonmoBuilder` for a single item in our list.
151///
152/// # Arguments
153/// * `index` - A `Signal<Item = Option<usize>>` that provides the current index of this item. This
154/// signal is provided by the `.enumerate()` call in `ui_root`.
155/// * `color` - The `Color` for this specific item.
156fn item(index: impl Signal<Item = Option<usize>> + Clone, color: Color) -> JonmoBuilder {
157 // --- The LazyEntity Pattern ---
158 // `LazyEntity` is a thread-safe, clone-able handle to an `Entity` that can be
159 // created *before* the entity is spawned.
160 // We need this because we want to create a signal for the text display that *reads*
161 // the `Lifetime` component from its own parent entity. When we define the text signal,
162 // the parent entity doesn't exist yet. `LazyEntity` acts as a promise that will be
163 // fulfilled later.
164 let lifetime_holder = LazyEntity::new();
165
166 JonmoBuilder::from((
167 Node {
168 height: Val::Px(40.0),
169 width: Val::Px(350.0),
170 align_items: AlignItems::Center,
171 flex_direction: FlexDirection::Row,
172 column_gap: Val::Px(10.0),
173 ..default()
174 },
175 // Each item gets its own `Lifetime` component, which will be updated by the `live` system.
176 Lifetime::default(),
177 ))
178 // Here we fulfill the promise. `entity_sync` will set the `Entity` id into the
179 // `lifetime_holder` once this `JonmoBuilder` is spawned into an actual entity.
180 // Any signals that were created using `lifetime_holder` will now point to the correct entity.
181 .entity_sync(lifetime_holder.clone())
182 .child({
183 // The main info panel for the item.
184 JonmoBuilder::from((
185 Node {
186 height: Val::Percent(100.),
187 width: Val::Percent(90.),
188 align_items: AlignItems::Center,
189 justify_content: JustifyContent::Center,
190 ..default()
191 },
192 BackgroundColor(color),
193 ))
194 .child(
195 // This `JonmoBuilder` will hold the text. Bevy UI text is composed of `TextSection`s,
196 // which are children of a `Text` entity. We use `JonmoBuilder`'s child methods to
197 // construct these sections reactively.
198 JonmoBuilder::from((
199 Node::default(),
200 // Start with a `Text` component with no sections. We'll add them as children.
201 Text::new(""),
202 TextColor(Color::BLACK), // Default color, can be overridden by children.
203 TextLayout::new_with_justify(JustifyText::Center),
204 ))
205 // Child 1: A static text span.
206 .child((TextColor(Color::BLACK), TextSpan::new("item ")))
207 // Child 2: A reactive text span for the index.
208 .child(
209 JonmoBuilder::from(TextColor(Color::BLACK)).component_signal(
210 // `component_signal` takes a signal and uses its output to insert/update a component.
211 index
212 .clone()
213 .map_in(|index| index.as_ref().map(ToString::to_string).map(TextSpan)),
214 ),
215 )
216 // Child 3: Another static text span.
217 .child((TextColor(Color::BLACK), TextSpan::new(" | lifetime: ")))
218 // Child 4: A reactive text span for the lifetime.
219 .child(
220 JonmoBuilder::from(TextColor(Color::BLACK)).component_signal(
221 // This is where the `LazyEntity` becomes powerful.
222 // We create a signal that reads a component from the entity that `lifetime_holder` will eventually
223 // point to.
224 SignalBuilder::from_component_lazy(lifetime_holder)
225 // Map the `Lifetime` component to its inner `f32` value and round it.
226 .map_in(|Lifetime(lifetime)| lifetime.round())
227 // `dedupe` is a crucial optimization. It ensures the rest of the signal chain only runs
228 // when the rounded lifetime value actually changes (once per second in this case),
229 // not on every single frame.
230 .dedupe()
231 // Convert the rounded `f32` to a `String`.
232 .map_in_ref(ToString::to_string)
233 // Wrap it in a `TextSpan` component for display.
234 .map_in(TextSpan)
235 .map_in(Some),
236 ),
237 ),
238 )
239 })
240 // Add the "remove" button as a child of the item row.
241 .child(
242 JonmoBuilder::from((
243 Node {
244 height: Val::Percent(100.),
245 width: Val::Percent(10.),
246 align_items: AlignItems::Center,
247 justify_content: JustifyContent::Center,
248 ..default()
249 },
250 // Using a color from Bevy's built-in palette for the button.
251 BackgroundColor(bevy::color::palettes::basic::RED.into()),
252 ))
253 // `on_spawn` runs a closure with access to the `World` and the spawned `Entity`
254 // just after the entity is created. This is a good place to set up observers or
255 // other one-time logic.
256 .on_spawn(|world, entity| {
257 // `observe` is a Bevy event-handling pattern. Here, we're setting up this
258 // button entity to listen for a `Click` event.
259 world.entity_mut(entity).observe(
260 // This closure is the event handler that runs when the button is clicked.
261 move |_: Trigger<Pointer<Click>>,
262 indices: Query<&Index>,
263 colors: Res<Colors>,
264 mut mutable_vec_datas: Query<&mut MutableVecData<_>>| {
265 // Try to get the `Index` component from the clicked entity.
266 if let Ok(&Index(index)) = indices.get(entity) {
267 // We found the index! Now we can mutate the central data source.
268 // `colors.0.write()` gets a write lock on the `MutableVec`.
269 colors.0.write(&mut mutable_vec_datas).remove(index);
270 }
271 },
272 );
273 })
274 // To make the observer work, the button entity needs to *have* an `Index` component.
275 // We use `component_signal` again to reactively insert the `Index` component,
276 // driven by the same `index` signal we used for the display text.
277 .component_signal(index.map_in(|index| index.map(Index)))
278 .child(JonmoBuilder::from((
279 Node::default(),
280 Text::new("x"),
281 TextColor(Color::WHITE),
282 TextLayout::new_with_justify(JustifyText::Center),
283 ))),
284 )
285}
286
287/// A standard Bevy system to spawn a 2D camera.
288fn camera(mut commands: Commands) {
289 commands.spawn(Camera2d);
290}
291
292/// This system runs every frame and is responsible for updating the `Lifetime`
293/// of every list item. This change is automatically picked up by the reactive
294/// text signal we defined in the `item` builder.
295fn live(mut lifetimes: Query<&mut Lifetime>, time: Res<Time>) {
296 for mut lifetime in lifetimes.iter_mut() {
297 lifetime.0 += time.delta_secs();
298 }
299}