filters/
filters.rs

1//! Diverse filtering options for a list of items, showcasing vector reactivity.
2
3mod utils;
4use utils::*;
5
6use bevy::{ecs::system::IntoObserverSystem, platform::collections::HashSet, prelude::*};
7use jonmo::{prelude::*, utils::SSs};
8use rand::{Rng, prelude::IndexedRandom};
9
10fn main() {
11    let mut app = App::new();
12    let world = app.world_mut();
13    let datas = MutableVecBuilder::from((0..12).map(|_| random_data()).collect::<Vec<_>>()).spawn(world);
14    let rows = MutableVecBuilder::from((0..5).map(|_| ()).collect::<Vec<_>>()).spawn(world);
15    app.add_plugins(examples_plugin)
16        .insert_resource(Datas(datas.clone()))
17        .insert_resource(Rows(rows.clone()))
18        .add_systems(
19            Startup,
20            (
21                move |world: &mut World| {
22                    ui(datas.clone(), rows.clone()).spawn(world);
23                },
24                camera,
25            ),
26        )
27        .run();
28}
29
30fn random_data() -> Data {
31    let mut rng = rand::rng();
32    Data {
33        number: rng.random_range(..100),
34        color: [ColorEnum::Blue, ColorEnum::Pink, ColorEnum::White]
35            .choose(&mut rng)
36            .copied()
37            .unwrap(),
38        shape: [Shape::Square, Shape::Circle].choose(&mut rng).copied().unwrap(),
39    }
40}
41
42#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
43enum ColorEnum {
44    Blue,
45    Pink,
46    White,
47}
48
49impl From<ColorEnum> for Color {
50    fn from(val: ColorEnum) -> Self {
51        match val {
52            ColorEnum::Blue => BLUE,
53            ColorEnum::Pink => PINK,
54            ColorEnum::White => Color::WHITE,
55        }
56    }
57}
58
59#[derive(Clone, Debug)]
60struct Data {
61    number: u32,
62    color: ColorEnum,
63    shape: Shape,
64}
65
66#[derive(Resource)]
67struct Datas(MutableVec<Data>);
68
69#[derive(Resource)]
70struct Rows(MutableVec<()>);
71
72#[derive(Component, Clone, PartialEq, Debug)]
73struct NumberFilters(HashSet<Parity>);
74
75#[derive(Component, Clone, PartialEq)]
76struct ColorFilters(HashSet<ColorEnum>);
77
78#[derive(Component, Clone, PartialEq)]
79struct ShapeFilters(HashSet<Shape>);
80
81#[derive(Component, Clone)]
82struct Sorted;
83
84const GAP: f32 = 5.;
85
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}
523
524fn camera(mut commands: Commands) {
525    commands.spawn(Camera2d);
526}