pub struct MutableVec<T> { /* private fields */ }Expand description
Implementations§
Source§impl<T> MutableVec<T>
impl<T> MutableVec<T>
Sourcepub fn read<'s>(
&self,
mutable_vec_data_reader: impl ReadMutableVecData<'s, T>,
) -> MutableVecReadGuard<'s, T>where
T: SSs,
pub fn read<'s>(
&self,
mutable_vec_data_reader: impl ReadMutableVecData<'s, T>,
) -> MutableVecReadGuard<'s, T>where
T: SSs,
Provides read-only access to the underlying Vec via either a &World or a
&Query<MutableVecData<T>>.
Sourcepub fn write<'w>(
&self,
mutable_vec_data_writer: impl WriteMutableVecData<'w, T>,
) -> MutableVecWriteGuard<'w, T>where
T: SSs,
pub fn write<'w>(
&self,
mutable_vec_data_writer: impl WriteMutableVecData<'w, T>,
) -> MutableVecWriteGuard<'w, T>where
T: SSs,
Provides write access to the underlying Vec via either a &mut World or a
&mut Query<&mut MutableVecData<T>>.
Examples found in repository?
examples/test.rs (line 144)
139fn hotkeys(
140 keys: Res<ButtonInput<KeyCode>>,
141 numbers: ResMut<Numbers>,
142 mut mutable_vec_datas: Query<&mut MutableVecData<i32>>,
143) {
144 let mut guard = numbers.0.write(&mut mutable_vec_datas);
145 if keys.just_pressed(KeyCode::Equal) {
146 guard.push((guard.len() + 1) as i32);
147 } else if keys.just_pressed(KeyCode::Minus) {
148 guard.pop();
149 }
150}More examples
examples/filters.rs (line 119)
86fn ui(items: MutableVec<Data>, rows: MutableVec<()>) -> JonmoBuilder {
87 JonmoBuilder::from(Node {
88 height: Val::Percent(100.),
89 width: Val::Percent(100.),
90 ..default()
91 })
92 .child(
93 JonmoBuilder::from(Node {
94 flex_direction: FlexDirection::Column,
95 align_self: AlignSelf::Start,
96 justify_self: JustifySelf::Start,
97 row_gap: Val::Px(GAP * 2.),
98 padding: UiRect::all(Val::Px(GAP * 4.)),
99 ..default()
100 })
101 .child(
102 JonmoBuilder::from((Node {
103 flex_direction: FlexDirection::Row,
104 align_items: AlignItems::Center,
105 column_gap: Val::Px(GAP * 2.),
106 ..default()
107 },))
108 .child(JonmoBuilder::from((
109 Node::default(),
110 Text::new("source"),
111 TextColor(Color::WHITE),
112 TextFont::from_font_size(30.),
113 TextLayout::new_with_justify(JustifyText::Center),
114 )))
115 .child(button("+", -2.).apply(on_click(
116 |_: Trigger<Pointer<Click>>,
117 datas: Res<Datas>,
118 mut mutable_vec_datas: Query<&mut MutableVecData<_>>| {
119 datas.0.write(&mut mutable_vec_datas).insert(0, random_data());
120 },
121 )))
122 .child(
123 JonmoBuilder::from(Node {
124 flex_direction: FlexDirection::Row,
125 align_items: AlignItems::Center,
126 column_gap: Val::Px(GAP),
127 ..default()
128 })
129 .children_signal_vec(
130 items
131 .signal_vec()
132 .enumerate()
133 .map_in(|(index, data)| item(index.dedupe(), data)),
134 ),
135 ),
136 )
137 .children_signal_vec(
138 rows.signal_vec()
139 .enumerate()
140 .map_in(clone!((items) move |(index, _)| row(index.dedupe(), items.clone()))),
141 )
142 .child(
143 JonmoBuilder::from((
144 Node {
145 height: Val::Px(55.),
146 justify_content: JustifyContent::Center,
147 flex_direction: FlexDirection::Column,
148 ..default()
149 },
150 // BackgroundColor(Color::WHITE),
151 ))
152 .child(button("+", -2.).apply(on_click(
153 |_: Trigger<Pointer<Click>>, rows: Res<Rows>, mut mutable_vec_datas: Query<&mut MutableVecData<_>>| {
154 rows.0.write(&mut mutable_vec_datas).push(());
155 },
156 ))),
157 ),
158 )
159}
160
161fn random_subset<T: Clone>(items: &[T]) -> Vec<T> {
162 let mut rng = rand::rng();
163 loop {
164 let subset: Vec<T> = items
165 .iter()
166 .filter(|_| rng.random_bool(0.5)) // "Flip a coin" for each item
167 .cloned() // Convert from `&&T` to `T`
168 .collect();
169
170 if !subset.is_empty() {
171 // If we have at least one item, return the subset
172 return subset;
173 }
174 // Otherwise, the loop continues and we try again
175 }
176}
177
178fn random_number_filters() -> NumberFilters {
179 NumberFilters(HashSet::from_iter(random_subset(&[Parity::Odd, Parity::Even])))
180}
181
182fn random_color_filters() -> ColorFilters {
183 ColorFilters(HashSet::from_iter(random_subset(&[
184 ColorEnum::Blue,
185 ColorEnum::Pink,
186 ColorEnum::White,
187 ])))
188}
189
190fn random_shape_filters() -> ShapeFilters {
191 ShapeFilters(HashSet::from_iter(random_subset(&[Shape::Square, Shape::Circle])))
192}
193
194fn maybe_insert_random_sorted(builder: JonmoBuilder) -> JonmoBuilder {
195 let mut rng = rand::rng();
196 if rng.random_bool(0.5) {
197 builder.insert(Sorted)
198 } else {
199 builder
200 }
201}
202
203fn text_node(text: &'static str) -> JonmoBuilder {
204 JonmoBuilder::from((
205 Node::default(),
206 Text::new(text),
207 TextColor(Color::WHITE),
208 TextLayout::new_with_justify(JustifyText::Center),
209 BorderRadius::all(Val::Px(GAP)),
210 ))
211}
212
213fn toggle<T: Eq + core::hash::Hash>(set: &mut HashSet<T>, value: T) {
214 if !set.remove(&value) {
215 set.insert(value);
216 }
217}
218
219fn on_click<M>(
220 on_click: impl IntoObserverSystem<Pointer<Click>, (), M> + SSs,
221) -> impl FnOnce(JonmoBuilder) -> JonmoBuilder {
222 move |builder: JonmoBuilder| {
223 builder.on_spawn(move |world, entity| {
224 world.entity_mut(entity).observe(on_click);
225 })
226 }
227}
228
229fn outline() -> Outline {
230 Outline {
231 width: Val::Px(1.),
232 ..default()
233 }
234}
235
236fn number_toggle(row_parent: LazyEntity, parity: Parity) -> impl Fn(JonmoBuilder) -> JonmoBuilder {
237 move |builder| {
238 builder
239 .apply(on_click(
240 clone!((row_parent) move |_: Trigger<Pointer<Click>>, mut number_filters: Query<&mut NumberFilters>| {
241 toggle(&mut number_filters.get_mut(row_parent.get()).unwrap().0, parity);
242 }),
243 ))
244 .component_signal(
245 SignalBuilder::from_component_lazy(row_parent.clone())
246 .dedupe()
247 .map_in(move |NumberFilters(filters)| filters.contains(&parity))
248 .dedupe()
249 .map_true(|_: In<()>| outline()),
250 )
251 }
252}
253
254fn number_toggles(row_parent: LazyEntity) -> JonmoBuilder {
255 JonmoBuilder::from(Node {
256 flex_direction: FlexDirection::Column,
257 row_gap: Val::Px(2.),
258 ..default()
259 })
260 .child(
261 text_node("even")
262 .insert(TextFont::from_font_size(13.))
263 .insert(BackgroundColor(bevy::color::palettes::basic::GRAY.into()))
264 .apply(number_toggle(row_parent.clone(), Parity::Even)),
265 )
266 .child(
267 text_node("odd")
268 .insert(TextFont::from_font_size(13.))
269 .insert(BackgroundColor(bevy::color::palettes::basic::GRAY.into()))
270 .apply(number_toggle(row_parent.clone(), Parity::Odd)),
271 )
272 .child(
273 text_node("sort")
274 .insert(TextFont::from_font_size(13.))
275 .insert(BackgroundColor(bevy::color::palettes::basic::GRAY.into()))
276 .apply(on_click(
277 clone!((row_parent) move |_: Trigger<Pointer<Click>>, world: &mut World| {
278 let mut entity = world.entity_mut(row_parent.get());
279 if entity.take::<Sorted>().is_none() { entity.insert(Sorted); }
280 }),
281 ))
282 .component_signal(
283 SignalBuilder::from_lazy_entity(row_parent.clone())
284 .has_component::<Sorted>()
285 .dedupe()
286 .map_true(|_: In<()>| outline()),
287 ),
288 )
289}
290
291fn shape_toggle(row_parent: LazyEntity, shape: Shape) -> JonmoBuilder {
292 JonmoBuilder::from((
293 Node {
294 width: Val::Px(20.),
295 height: Val::Px(20.),
296 ..default()
297 },
298 BackgroundColor(bevy::color::palettes::basic::GRAY.into()),
299 ))
300 .apply(on_click(
301 clone!((row_parent) move |_: Trigger<Pointer<Click>>, mut shape_filters: Query<&mut ShapeFilters>| {
302 toggle(&mut shape_filters.get_mut(row_parent.get()).unwrap().0, shape);
303 }),
304 ))
305 .component_signal(
306 SignalBuilder::from_component_lazy(row_parent.clone())
307 .dedupe()
308 .map_in(move |ShapeFilters(filters)| filters.contains(&shape))
309 .dedupe()
310 .map_true(|_: In<()>| outline()),
311 )
312}
313
314fn shape_toggles(row_parent: LazyEntity) -> JonmoBuilder {
315 JonmoBuilder::from(Node {
316 flex_direction: FlexDirection::Column,
317 justify_content: JustifyContent::Center,
318 row_gap: Val::Px(GAP),
319 ..default()
320 })
321 .child(shape_toggle(row_parent.clone(), Shape::Square))
322 .child(shape_toggle(row_parent.clone(), Shape::Circle).insert(BorderRadius::MAX))
323}
324
325fn color_toggles(row_parent: LazyEntity) -> JonmoBuilder {
326 JonmoBuilder::from(Node {
327 flex_direction: FlexDirection::Column,
328 justify_content: JustifyContent::Center,
329 row_gap: Val::Px(GAP),
330 ..default()
331 })
332 .children(
333 [ColorEnum::Blue, ColorEnum::Pink, ColorEnum::White]
334 .into_iter()
335 .map(move |color| {
336 JonmoBuilder::from((
337 Node {
338 width: Val::Px(15.),
339 height: Val::Px(15.),
340 border: UiRect::all(Val::Px(1.)),
341 ..default()
342 },
343 BorderRadius::all(Val::Px(GAP)),
344 BackgroundColor(color.into()),
345 BorderColor(Color::BLACK),
346 ))
347 .apply(on_click(
348 clone!((row_parent) move |_: Trigger<Pointer<Click>>, mut color_filters: Query<&mut ColorFilters>| {
349 toggle(&mut color_filters.get_mut(row_parent.get()).unwrap().0, color);
350 }),
351 ))
352 .component_signal(
353 SignalBuilder::from_component_lazy(row_parent.clone())
354 .dedupe()
355 .map_in(move |ColorFilters(filters)| filters.contains(&color))
356 .dedupe()
357 .map_true(|_: In<()>| outline()),
358 )
359 }),
360 )
361}
362
363fn button(text: &'static str, offset: f32) -> JonmoBuilder {
364 JonmoBuilder::from((
365 Node {
366 width: Val::Px((ITEM_SIZE / 2) as f32),
367 height: Val::Px((ITEM_SIZE / 2) as f32),
368 justify_content: JustifyContent::Center,
369 border: UiRect::all(Val::Px(1.)),
370 ..default()
371 },
372 BackgroundColor(bevy::color::palettes::basic::GRAY.into()),
373 BorderColor(Color::WHITE),
374 BorderRadius::all(Val::Px(GAP)),
375 ))
376 .child(
377 text_node(text)
378 .with_component::<Node>(move |mut node| node.top = Val::Px(offset))
379 .insert(TextFont::from_font_size(24.)),
380 )
381}
382
383#[derive(Component, Clone)]
384struct Index(usize);
385
386fn row(index: impl Signal<Item = Option<usize>>, items: MutableVec<Data>) -> JonmoBuilder {
387 let row_parent = LazyEntity::new();
388 JonmoBuilder::from((
389 Node {
390 flex_direction: FlexDirection::Row,
391 align_items: AlignItems::Center,
392 column_gap: Val::Px(GAP * 2.),
393 ..default()
394 },
395 random_number_filters(),
396 random_color_filters(),
397 random_shape_filters(),
398 ))
399 .apply(maybe_insert_random_sorted)
400 .entity_sync(row_parent.clone())
401 .child(
402 button("-", -3.)
403 .component_signal(index.map_in(|index| index.map(Index)))
404 .apply(on_click(
405 |click: Trigger<Pointer<Click>>,
406 rows: Res<Rows>,
407 indices: Query<&Index>,
408 mut mutable_vec_datas: Query<&mut MutableVecData<_>>| {
409 if let Ok(&Index(index)) = indices.get(click.target()) {
410 rows.0.write(&mut mutable_vec_datas).remove(index);
411 }
412 },
413 )),
414 )
415 .child(
416 JonmoBuilder::from((Node {
417 flex_direction: FlexDirection::Row,
418 width: Val::Px(108.),
419 height: Val::Percent(100.),
420 column_gap: Val::Px(GAP * 2.),
421 justify_content: JustifyContent::Center,
422 ..default()
423 },))
424 .child(number_toggles(row_parent.clone()))
425 .child(shape_toggles(row_parent.clone()))
426 .child(color_toggles(row_parent.clone())),
427 )
428 .child(
429 JonmoBuilder::from((Node {
430 flex_direction: FlexDirection::Row,
431 align_items: AlignItems::Center,
432 column_gap: Val::Px(GAP),
433 ..default()
434 },))
435 .children_signal_vec(
436 SignalBuilder::from_lazy_entity(row_parent.clone())
437 .has_component::<Sorted>()
438 .dedupe()
439 .switch_signal_vec(move |In(sorted)| {
440 let base = items.signal_vec().enumerate();
441 if sorted {
442 base.sort_by_key(|In((_, Data { number, .. }))| number).left_either()
443 } else {
444 base.right_either()
445 }
446 })
447 .filter_signal(clone!((row_parent) move | In((_, Data { number, .. })) | {
448 SignalBuilder::from_component_lazy(row_parent.clone())
449 .dedupe()
450 .map_in(move |number_filters: NumberFilters| {
451 number_filters.0.contains(&if number.is_multiple_of(2) {
452 Parity::Even
453 } else {
454 Parity::Odd
455 })
456 })
457 .dedupe()
458 }))
459 .filter_signal(clone!((row_parent) move | In((_, Data { shape, .. })) | {
460 SignalBuilder::from_component_lazy(row_parent.clone())
461 .dedupe()
462 .map_in(move |shape_filters: ShapeFilters| shape_filters.0.contains(&shape))
463 .dedupe()
464 }))
465 .filter_signal(clone!((row_parent) move | In((_, Data { color, .. })) | {
466 SignalBuilder::from_component_lazy(row_parent.clone())
467 .dedupe()
468 .map_in(move |color_filters: ColorFilters| color_filters.0.contains(&color))
469 .dedupe()
470 }))
471 .map_in(|(index, data)| item(index.dedupe(), data)),
472 ),
473 )
474}
475
476const ITEM_SIZE: u32 = 50;
477
478#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
479enum Shape {
480 Square,
481 Circle,
482}
483
484#[derive(Clone, Copy, PartialEq, Hash, Eq, Debug)]
485enum Parity {
486 Even,
487 Odd,
488}
489
490fn item(index: impl Signal<Item = Option<usize>>, Data { number, color, shape }: Data) -> JonmoBuilder {
491 JonmoBuilder::from((
492 Node {
493 height: Val::Px(ITEM_SIZE as f32),
494 width: Val::Px(ITEM_SIZE as f32),
495 align_items: AlignItems::Center,
496 justify_content: JustifyContent::Center,
497 ..default()
498 },
499 BackgroundColor(color.into()),
500 match shape {
501 Shape::Square => BorderRadius::default(),
502 Shape::Circle => BorderRadius::MAX,
503 },
504 ))
505 .component_signal(index.map_in(|index| index.map(Index)))
506 .apply(on_click(
507 |click: Trigger<Pointer<Click>>,
508 datas: Res<Datas>,
509 indices: Query<&Index>,
510 mut mutable_vec_datas: Query<&mut MutableVecData<_>>| {
511 if let Ok(&Index(index)) = indices.get(click.target()) {
512 datas.0.write(&mut mutable_vec_datas).remove(index);
513 }
514 },
515 ))
516 .child((
517 Node::default(),
518 Text::new(number.to_string()),
519 TextColor(Color::BLACK),
520 TextLayout::new_with_justify(JustifyText::Center),
521 ))
522}examples/lifetimes.rs (line 130)
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}Sourcepub fn signal_vec(&self) -> Source<T>
pub fn signal_vec(&self) -> Source<T>
Returns a Source signal from this MutableVec.
Examples found in repository?
examples/lifetimes.rs (line 48)
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}More examples
examples/filters.rs (line 131)
86fn ui(items: MutableVec<Data>, rows: MutableVec<()>) -> JonmoBuilder {
87 JonmoBuilder::from(Node {
88 height: Val::Percent(100.),
89 width: Val::Percent(100.),
90 ..default()
91 })
92 .child(
93 JonmoBuilder::from(Node {
94 flex_direction: FlexDirection::Column,
95 align_self: AlignSelf::Start,
96 justify_self: JustifySelf::Start,
97 row_gap: Val::Px(GAP * 2.),
98 padding: UiRect::all(Val::Px(GAP * 4.)),
99 ..default()
100 })
101 .child(
102 JonmoBuilder::from((Node {
103 flex_direction: FlexDirection::Row,
104 align_items: AlignItems::Center,
105 column_gap: Val::Px(GAP * 2.),
106 ..default()
107 },))
108 .child(JonmoBuilder::from((
109 Node::default(),
110 Text::new("source"),
111 TextColor(Color::WHITE),
112 TextFont::from_font_size(30.),
113 TextLayout::new_with_justify(JustifyText::Center),
114 )))
115 .child(button("+", -2.).apply(on_click(
116 |_: Trigger<Pointer<Click>>,
117 datas: Res<Datas>,
118 mut mutable_vec_datas: Query<&mut MutableVecData<_>>| {
119 datas.0.write(&mut mutable_vec_datas).insert(0, random_data());
120 },
121 )))
122 .child(
123 JonmoBuilder::from(Node {
124 flex_direction: FlexDirection::Row,
125 align_items: AlignItems::Center,
126 column_gap: Val::Px(GAP),
127 ..default()
128 })
129 .children_signal_vec(
130 items
131 .signal_vec()
132 .enumerate()
133 .map_in(|(index, data)| item(index.dedupe(), data)),
134 ),
135 ),
136 )
137 .children_signal_vec(
138 rows.signal_vec()
139 .enumerate()
140 .map_in(clone!((items) move |(index, _)| row(index.dedupe(), items.clone()))),
141 )
142 .child(
143 JonmoBuilder::from((
144 Node {
145 height: Val::Px(55.),
146 justify_content: JustifyContent::Center,
147 flex_direction: FlexDirection::Column,
148 ..default()
149 },
150 // BackgroundColor(Color::WHITE),
151 ))
152 .child(button("+", -2.).apply(on_click(
153 |_: Trigger<Pointer<Click>>, rows: Res<Rows>, mut mutable_vec_datas: Query<&mut MutableVecData<_>>| {
154 rows.0.write(&mut mutable_vec_datas).push(());
155 },
156 ))),
157 ),
158 )
159}
160
161fn random_subset<T: Clone>(items: &[T]) -> Vec<T> {
162 let mut rng = rand::rng();
163 loop {
164 let subset: Vec<T> = items
165 .iter()
166 .filter(|_| rng.random_bool(0.5)) // "Flip a coin" for each item
167 .cloned() // Convert from `&&T` to `T`
168 .collect();
169
170 if !subset.is_empty() {
171 // If we have at least one item, return the subset
172 return subset;
173 }
174 // Otherwise, the loop continues and we try again
175 }
176}
177
178fn random_number_filters() -> NumberFilters {
179 NumberFilters(HashSet::from_iter(random_subset(&[Parity::Odd, Parity::Even])))
180}
181
182fn random_color_filters() -> ColorFilters {
183 ColorFilters(HashSet::from_iter(random_subset(&[
184 ColorEnum::Blue,
185 ColorEnum::Pink,
186 ColorEnum::White,
187 ])))
188}
189
190fn random_shape_filters() -> ShapeFilters {
191 ShapeFilters(HashSet::from_iter(random_subset(&[Shape::Square, Shape::Circle])))
192}
193
194fn maybe_insert_random_sorted(builder: JonmoBuilder) -> JonmoBuilder {
195 let mut rng = rand::rng();
196 if rng.random_bool(0.5) {
197 builder.insert(Sorted)
198 } else {
199 builder
200 }
201}
202
203fn text_node(text: &'static str) -> JonmoBuilder {
204 JonmoBuilder::from((
205 Node::default(),
206 Text::new(text),
207 TextColor(Color::WHITE),
208 TextLayout::new_with_justify(JustifyText::Center),
209 BorderRadius::all(Val::Px(GAP)),
210 ))
211}
212
213fn toggle<T: Eq + core::hash::Hash>(set: &mut HashSet<T>, value: T) {
214 if !set.remove(&value) {
215 set.insert(value);
216 }
217}
218
219fn on_click<M>(
220 on_click: impl IntoObserverSystem<Pointer<Click>, (), M> + SSs,
221) -> impl FnOnce(JonmoBuilder) -> JonmoBuilder {
222 move |builder: JonmoBuilder| {
223 builder.on_spawn(move |world, entity| {
224 world.entity_mut(entity).observe(on_click);
225 })
226 }
227}
228
229fn outline() -> Outline {
230 Outline {
231 width: Val::Px(1.),
232 ..default()
233 }
234}
235
236fn number_toggle(row_parent: LazyEntity, parity: Parity) -> impl Fn(JonmoBuilder) -> JonmoBuilder {
237 move |builder| {
238 builder
239 .apply(on_click(
240 clone!((row_parent) move |_: Trigger<Pointer<Click>>, mut number_filters: Query<&mut NumberFilters>| {
241 toggle(&mut number_filters.get_mut(row_parent.get()).unwrap().0, parity);
242 }),
243 ))
244 .component_signal(
245 SignalBuilder::from_component_lazy(row_parent.clone())
246 .dedupe()
247 .map_in(move |NumberFilters(filters)| filters.contains(&parity))
248 .dedupe()
249 .map_true(|_: In<()>| outline()),
250 )
251 }
252}
253
254fn number_toggles(row_parent: LazyEntity) -> JonmoBuilder {
255 JonmoBuilder::from(Node {
256 flex_direction: FlexDirection::Column,
257 row_gap: Val::Px(2.),
258 ..default()
259 })
260 .child(
261 text_node("even")
262 .insert(TextFont::from_font_size(13.))
263 .insert(BackgroundColor(bevy::color::palettes::basic::GRAY.into()))
264 .apply(number_toggle(row_parent.clone(), Parity::Even)),
265 )
266 .child(
267 text_node("odd")
268 .insert(TextFont::from_font_size(13.))
269 .insert(BackgroundColor(bevy::color::palettes::basic::GRAY.into()))
270 .apply(number_toggle(row_parent.clone(), Parity::Odd)),
271 )
272 .child(
273 text_node("sort")
274 .insert(TextFont::from_font_size(13.))
275 .insert(BackgroundColor(bevy::color::palettes::basic::GRAY.into()))
276 .apply(on_click(
277 clone!((row_parent) move |_: Trigger<Pointer<Click>>, world: &mut World| {
278 let mut entity = world.entity_mut(row_parent.get());
279 if entity.take::<Sorted>().is_none() { entity.insert(Sorted); }
280 }),
281 ))
282 .component_signal(
283 SignalBuilder::from_lazy_entity(row_parent.clone())
284 .has_component::<Sorted>()
285 .dedupe()
286 .map_true(|_: In<()>| outline()),
287 ),
288 )
289}
290
291fn shape_toggle(row_parent: LazyEntity, shape: Shape) -> JonmoBuilder {
292 JonmoBuilder::from((
293 Node {
294 width: Val::Px(20.),
295 height: Val::Px(20.),
296 ..default()
297 },
298 BackgroundColor(bevy::color::palettes::basic::GRAY.into()),
299 ))
300 .apply(on_click(
301 clone!((row_parent) move |_: Trigger<Pointer<Click>>, mut shape_filters: Query<&mut ShapeFilters>| {
302 toggle(&mut shape_filters.get_mut(row_parent.get()).unwrap().0, shape);
303 }),
304 ))
305 .component_signal(
306 SignalBuilder::from_component_lazy(row_parent.clone())
307 .dedupe()
308 .map_in(move |ShapeFilters(filters)| filters.contains(&shape))
309 .dedupe()
310 .map_true(|_: In<()>| outline()),
311 )
312}
313
314fn shape_toggles(row_parent: LazyEntity) -> JonmoBuilder {
315 JonmoBuilder::from(Node {
316 flex_direction: FlexDirection::Column,
317 justify_content: JustifyContent::Center,
318 row_gap: Val::Px(GAP),
319 ..default()
320 })
321 .child(shape_toggle(row_parent.clone(), Shape::Square))
322 .child(shape_toggle(row_parent.clone(), Shape::Circle).insert(BorderRadius::MAX))
323}
324
325fn color_toggles(row_parent: LazyEntity) -> JonmoBuilder {
326 JonmoBuilder::from(Node {
327 flex_direction: FlexDirection::Column,
328 justify_content: JustifyContent::Center,
329 row_gap: Val::Px(GAP),
330 ..default()
331 })
332 .children(
333 [ColorEnum::Blue, ColorEnum::Pink, ColorEnum::White]
334 .into_iter()
335 .map(move |color| {
336 JonmoBuilder::from((
337 Node {
338 width: Val::Px(15.),
339 height: Val::Px(15.),
340 border: UiRect::all(Val::Px(1.)),
341 ..default()
342 },
343 BorderRadius::all(Val::Px(GAP)),
344 BackgroundColor(color.into()),
345 BorderColor(Color::BLACK),
346 ))
347 .apply(on_click(
348 clone!((row_parent) move |_: Trigger<Pointer<Click>>, mut color_filters: Query<&mut ColorFilters>| {
349 toggle(&mut color_filters.get_mut(row_parent.get()).unwrap().0, color);
350 }),
351 ))
352 .component_signal(
353 SignalBuilder::from_component_lazy(row_parent.clone())
354 .dedupe()
355 .map_in(move |ColorFilters(filters)| filters.contains(&color))
356 .dedupe()
357 .map_true(|_: In<()>| outline()),
358 )
359 }),
360 )
361}
362
363fn button(text: &'static str, offset: f32) -> JonmoBuilder {
364 JonmoBuilder::from((
365 Node {
366 width: Val::Px((ITEM_SIZE / 2) as f32),
367 height: Val::Px((ITEM_SIZE / 2) as f32),
368 justify_content: JustifyContent::Center,
369 border: UiRect::all(Val::Px(1.)),
370 ..default()
371 },
372 BackgroundColor(bevy::color::palettes::basic::GRAY.into()),
373 BorderColor(Color::WHITE),
374 BorderRadius::all(Val::Px(GAP)),
375 ))
376 .child(
377 text_node(text)
378 .with_component::<Node>(move |mut node| node.top = Val::Px(offset))
379 .insert(TextFont::from_font_size(24.)),
380 )
381}
382
383#[derive(Component, Clone)]
384struct Index(usize);
385
386fn row(index: impl Signal<Item = Option<usize>>, items: MutableVec<Data>) -> JonmoBuilder {
387 let row_parent = LazyEntity::new();
388 JonmoBuilder::from((
389 Node {
390 flex_direction: FlexDirection::Row,
391 align_items: AlignItems::Center,
392 column_gap: Val::Px(GAP * 2.),
393 ..default()
394 },
395 random_number_filters(),
396 random_color_filters(),
397 random_shape_filters(),
398 ))
399 .apply(maybe_insert_random_sorted)
400 .entity_sync(row_parent.clone())
401 .child(
402 button("-", -3.)
403 .component_signal(index.map_in(|index| index.map(Index)))
404 .apply(on_click(
405 |click: Trigger<Pointer<Click>>,
406 rows: Res<Rows>,
407 indices: Query<&Index>,
408 mut mutable_vec_datas: Query<&mut MutableVecData<_>>| {
409 if let Ok(&Index(index)) = indices.get(click.target()) {
410 rows.0.write(&mut mutable_vec_datas).remove(index);
411 }
412 },
413 )),
414 )
415 .child(
416 JonmoBuilder::from((Node {
417 flex_direction: FlexDirection::Row,
418 width: Val::Px(108.),
419 height: Val::Percent(100.),
420 column_gap: Val::Px(GAP * 2.),
421 justify_content: JustifyContent::Center,
422 ..default()
423 },))
424 .child(number_toggles(row_parent.clone()))
425 .child(shape_toggles(row_parent.clone()))
426 .child(color_toggles(row_parent.clone())),
427 )
428 .child(
429 JonmoBuilder::from((Node {
430 flex_direction: FlexDirection::Row,
431 align_items: AlignItems::Center,
432 column_gap: Val::Px(GAP),
433 ..default()
434 },))
435 .children_signal_vec(
436 SignalBuilder::from_lazy_entity(row_parent.clone())
437 .has_component::<Sorted>()
438 .dedupe()
439 .switch_signal_vec(move |In(sorted)| {
440 let base = items.signal_vec().enumerate();
441 if sorted {
442 base.sort_by_key(|In((_, Data { number, .. }))| number).left_either()
443 } else {
444 base.right_either()
445 }
446 })
447 .filter_signal(clone!((row_parent) move | In((_, Data { number, .. })) | {
448 SignalBuilder::from_component_lazy(row_parent.clone())
449 .dedupe()
450 .map_in(move |number_filters: NumberFilters| {
451 number_filters.0.contains(&if number.is_multiple_of(2) {
452 Parity::Even
453 } else {
454 Parity::Odd
455 })
456 })
457 .dedupe()
458 }))
459 .filter_signal(clone!((row_parent) move | In((_, Data { shape, .. })) | {
460 SignalBuilder::from_component_lazy(row_parent.clone())
461 .dedupe()
462 .map_in(move |shape_filters: ShapeFilters| shape_filters.0.contains(&shape))
463 .dedupe()
464 }))
465 .filter_signal(clone!((row_parent) move | In((_, Data { color, .. })) | {
466 SignalBuilder::from_component_lazy(row_parent.clone())
467 .dedupe()
468 .map_in(move |color_filters: ColorFilters| color_filters.0.contains(&color))
469 .dedupe()
470 }))
471 .map_in(|(index, data)| item(index.dedupe(), data)),
472 ),
473 )
474}examples/test.rs (line 36)
35fn ui_root(numbers: MutableVec<i32>, world: &mut World) -> JonmoBuilder {
36 let list_a = MutableVecBuilder::from([1, 2, 3, 4, 5]).spawn(world).signal_vec();
37 let list_b = MutableVecBuilder::from([3, 4, 5]).spawn(world).signal_vec();
38 let map = MutableBTreeMapBuilder::from([(1, 2), (2, 3)]).spawn(world);
39 // map.signal_map().
40 JonmoBuilder::from(Node {
41 height: Val::Percent(100.0),
42 width: Val::Percent(100.0),
43 flex_direction: FlexDirection::Column,
44 align_items: AlignItems::Center,
45 justify_content: JustifyContent::Center,
46 row_gap: Val::Px(10.0),
47 ..default()
48 })
49 // .child_signal(numbers.clone().is_empty().map(|In(len)| item(len as u32)))
50 .children_signal_vec(
51 // MutableVec::from([numbers.signal_vec(), numbers.signal_vec()]).signal_vec()
52 numbers
53 .signal_vec()
54 // SignalBuilder::from_system(|_: In<()>| 1)
55 // SignalBuilder::from_system(|_: In<()>, toggle: Res<ToggleFilter>| toggle.0)
56 // .dedupe()
57 // .switch_signal_vec(move |In(toggle)| {
58 // if toggle {
59 // // println!("toggled to A");
60 // list_a.clone()
61 // } else {
62 // // println!("toggled to B");
63 // list_b.clone()
64 // }
65 // })
66 // .dedupe()
67 // .to_signal_vec()
68 .filter_signal(|In(n)| {
69 SignalBuilder::from_system(move |_: In<()>, toggle: Res<ToggleFilter>| {
70 n % 2 == if toggle.0 { 0 } else { 1 }
71 })
72 })
73 // .map_signal(|In(n): In<i32>| {
74 // SignalBuilder::from_system(move |_: In<()>| n + 1).dedupe()
75 // })
76 // .debug()
77 // .map_in(|n: i32| -n)
78 // .sort_by_cmp()
79 // .flatten()
80 // .chain(numbers)
81 // .intersperse(0)
82 // .sort_by(|In((left, right)): In<(i32, i32)>| left.cmp(&right).reverse())
83 // .sort_by_key(|In(n): In<i32>| -n)
84 // .intersperse_with(|In(index_signal): In<jonmo::signal::Dedupe<jonmo::signal::Source<Option<usize>>>>, world: &mut World| {
85 // let signal = index_signal.debug().register(world);
86 // poll_signal(world, *signal)
87 // .and_then(downcast_any_clone::<Option<usize>>).flatten().unwrap_or_default() as i32
88 // })
89 .map(|In(n)| item(n))
90 // .intersperse_with(
91 // |index_signal: In<jonmo::signal::Dedupe<jonmo::signal::Source<Option<usize>>>>| {
92 // JonmoBuilder::from(Node::default()).component_signal(
93 // index_signal
94 // .debug()
95 // .map_in(|idx_opt| Text::new(format!("{}", idx_opt.unwrap_or(0)))),
96 // )
97 // },
98 // ),
99 )
100}Trait Implementations§
Source§impl<T> Clone for MutableVec<T>
impl<T> Clone for MutableVec<T>
Source§impl<T> Drop for MutableVec<T>
impl<T> Drop for MutableVec<T>
Source§impl<T> From<&mut Commands<'_, '_>> for MutableVec<T>
impl<T> From<&mut Commands<'_, '_>> for MutableVec<T>
Source§impl<T> From<&mut World> for MutableVec<T>
impl<T> From<&mut World> for MutableVec<T>
Auto Trait Implementations§
impl<T> Freeze for MutableVec<T>
impl<T> RefUnwindSafe for MutableVec<T>
impl<T> Send for MutableVec<T>
impl<T> Sync for MutableVec<T>
impl<T> Unpin for MutableVec<T>
impl<T> UnwindSafe for MutableVec<T>
Blanket Implementations§
Source§impl<T, Res> Apply<Res> for Twhere
T: ?Sized,
impl<T, Res> Apply<Res> for Twhere
T: ?Sized,
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Converts
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>, which can then be
downcast into Box<dyn ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Converts
Rc<Trait> (where Trait: Downcast) to Rc<Any>, which can then be further
downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
Converts
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
Converts
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.