1mod 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 ))
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)) .cloned() .collect();
169
170 if !subset.is_empty() {
171 return subset;
173 }
174 }
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}