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}