Skip to main content

letters/
letters.rs

1//! Key press counter with swappable save states, showcasing map reactivity.
2mod utils;
3use utils::*;
4
5extern crate alloc;
6use alloc::collections::BTreeMap;
7
8use bevy_platform::collections::HashMap;
9
10use bevy::{prelude::*, window::WindowResolution};
11use jonmo::prelude::*;
12
13const SAVE_CHARS: &str = "abcdefgh";
14
15fn main() {
16    let mut app = App::new();
17
18    let save_states: HashMap<_, _> = SAVE_CHARS
19        .chars()
20        .map(|save_char| {
21            (
22                save_char,
23                MutableBTreeMap::builder()
24                    .values(
25                        ROWS.iter()
26                            .flat_map(|row| row.chars().map(|letter| (letter, LetterData::default())))
27                            .collect::<BTreeMap<_, _>>(),
28                    )
29                    .spawn(app.world_mut()),
30            )
31        })
32        .collect();
33
34    app.add_plugins(examples_plugin)
35        .insert_resource(SaveStates(save_states))
36        .insert_resource(ActiveSave('a'))
37        .add_systems(
38            Startup,
39            (
40                |world: &mut World| {
41                    ui_root().spawn(world);
42                },
43                camera,
44            ),
45        )
46        .add_systems(Update, listen)
47        .run();
48}
49
50const ROWS: [&str; 3] = ["qwertyuiop", "asdfghjkl", "zxcvbnm"];
51
52#[derive(Default, Clone, Debug)]
53struct LetterData {
54    count: usize,
55    pressed: bool,
56}
57
58#[derive(Resource, Clone)]
59struct SaveStates(HashMap<char, MutableBTreeMap<char, LetterData>>);
60
61#[derive(Resource, Clone, Copy, PartialEq)]
62struct ActiveSave(char);
63
64fn get_active_map(save_states: &SaveStates, active_save: ActiveSave) -> MutableBTreeMap<char, LetterData> {
65    save_states.0.get(&active_save.0).unwrap().clone()
66}
67
68const LETTER_SIZE: f32 = 60.;
69
70fn ui_root() -> jonmo::Builder {
71    let active_save = signal::from_resource_changed::<ActiveSave>();
72
73    jonmo::Builder::from(Node {
74        height: Val::Percent(100.0),
75        width: Val::Percent(100.0),
76        justify_content: JustifyContent::Center,
77        ..default()
78    })
79    .child(
80        jonmo::Builder::from(Node {
81            flex_direction: FlexDirection::Column,
82            row_gap: Val::Px(GAP * 2.),
83            padding: UiRect::all(Val::Px(GAP * 4.)),
84            width: Val::Px(WindowResolution::default().physical_width() as f32),
85            justify_content: JustifyContent::Center,
86            ..default()
87        })
88        .child(
89            jonmo::Builder::from(Node {
90                align_self: AlignSelf::Center,
91                width: Val::Percent(100.),
92                ..default()
93            })
94            .child(
95                jonmo::Builder::from(Node {
96                    flex_direction: FlexDirection::Row,
97                    column_gap: Val::Px(GAP * 2.),
98                    ..default()
99                })
100                .children(SAVE_CHARS.chars().map(clone!((active_save) move |save_char| {
101                    save_card(save_char, active_save.clone())
102                }))),
103            )
104            .child(
105                sum_container()
106                .child(
107                    text_node()
108                        .insert((TextColor(BLUE), TextFont::from_font_size(LETTER_SIZE)))
109                        .with_component::<Node>(|mut node| node.height = Val::Px(100.))
110                        .component_signal(
111                            active_save
112                                .clone()
113                                .switch_signal_vec(move |In(active_save): In<ActiveSave>, save_states: Res<SaveStates>| {
114                                    get_active_map(&save_states, active_save).signal_vec_entries()
115                                })
116                                .map_in(|(_, LetterData { count, .. })| count)
117                                .sum()
118                                .dedupe()
119                                .map_in_ref(ToString::to_string)
120                                .map_in(Text)
121                                .map_in(Some),
122                        )
123                ),
124            ),
125        )
126        .children(ROWS.into_iter().map(clone!((active_save) move |row| {
127            jonmo::Builder::from(Node {
128                flex_direction: FlexDirection::Row,
129                column_gap: Val::Px(GAP * 2.),
130                ..default()
131            })
132            .children(row.chars().map(
133                clone!((active_save) move |l| {
134                    let letter_data = active_save
135                        .clone()
136                        .switch_signal_map(move |In(active_save): In<ActiveSave>, save_states: Res<SaveStates>| {
137                            get_active_map(&save_states, active_save).signal_map()
138                        })
139                        .key(l)
140                        .map_in(Option::unwrap_or_default);
141                    letter(l, letter_data)
142                }),
143            ))
144            .child(
145                sum_container()
146                .child(
147                    text_node()
148                        .insert((TextColor(BLUE), TextFont::from_font_size(LETTER_SIZE)))
149                        .component_signal(
150                            active_save
151                                .clone()
152                                .switch_signal_vec(move |In(active_save): In<ActiveSave>, save_states: Res<SaveStates>| {
153                                    get_active_map(&save_states, active_save).signal_vec_entries()
154                                })
155                                .filter(move |In((letter, _))| row.contains(letter))
156                                .map_in(|(_, LetterData { count, .. })| count)
157                                .sum()
158                                .dedupe()
159                                .map_in_ref(ToString::to_string)
160                                .map_in(Text)
161                                .map_in(Some),
162                        ),
163                ),
164            )
165        }))),
166    )
167}
168
169const GAP: f32 = 5.;
170
171fn sum_container() -> jonmo::Builder {
172    jonmo::Builder::from(Node {
173        align_self: AlignSelf::Center,
174        justify_content: JustifyContent::FlexEnd,
175        flex_grow: 1.,
176        padding: UiRect::all(Val::Px(GAP * 2.)),
177        ..default()
178    })
179}
180
181fn save_card(save_char: char, active_save_signal: impl Signal<Item = ActiveSave> + Clone) -> jonmo::Builder {
182    jonmo::Builder::from((Node {
183        width: Val::Px(100.),
184        height: Val::Px(100.),
185        justify_content: JustifyContent::Center,
186        align_items: AlignItems::Center,
187        padding: UiRect::all(Val::Px(GAP * 2.)),
188        border_radius: BorderRadius::all(Val::Px(GAP * 2.)),
189        ..default()
190    },))
191    .observe(move |_click: On<Pointer<Click>>, mut active_save: ResMut<ActiveSave>| {
192        active_save.0 = save_char;
193    })
194    .component_signal(
195        active_save_signal
196            .map_in(move |ActiveSave(active_char)| {
197                if active_char == save_char {
198                    BackgroundColor(BLUE)
199                } else {
200                    BackgroundColor(PINK)
201                }
202            })
203            .map_in(Some),
204    )
205    .child(text_node().insert((
206        Text(save_char.to_string()),
207        TextColor(Color::WHITE),
208        TextFont::from_font_size(LETTER_SIZE),
209    )))
210}
211
212fn text_node() -> jonmo::Builder {
213    jonmo::Builder::from((
214        Node {
215            border_radius: BorderRadius::all(Val::Px(GAP)),
216            ..default()
217        },
218        TextColor(Color::WHITE),
219        TextLayout::new_with_justify(Justify::Center),
220    ))
221}
222
223fn letter(letter: char, data: impl Signal<Item = LetterData> + Clone) -> jonmo::Builder {
224    jonmo::Builder::from((Node {
225        flex_direction: FlexDirection::Column,
226        row_gap: Val::Px(GAP * 2.),
227        padding: UiRect::all(Val::Px(GAP * 2.)),
228        width: Val::Px(100.),
229        border_radius: BorderRadius::all(Val::Px(GAP * 2.)),
230        ..default()
231    },))
232    .component_signal(
233        data.clone()
234            .map_in(|LetterData { pressed, .. }| pressed)
235            .dedupe()
236            .map_true_in(|| Outline {
237                width: Val::Px(1.),
238                ..default()
239            }),
240    )
241    .child(text_node().insert((
242        Text(letter.to_string()),
243        TextColor(PINK),
244        TextFont::from_font_size(LETTER_SIZE),
245    )))
246    .child(
247        text_node()
248            .insert(TextFont::from_font_size(LETTER_SIZE / 1.5))
249            .component_signal(
250                data.clone()
251                    .map_in(|LetterData { count, .. }| count)
252                    .dedupe()
253                    .map_in_ref(ToString::to_string)
254                    .map_in(Text)
255                    .map_in(Some),
256            ),
257    )
258}
259
260fn listen(
261    keys: ResMut<ButtonInput<KeyCode>>,
262    save_states: Res<SaveStates>,
263    active_save: Res<ActiveSave>,
264    mut mutable_btree_map_datas: Query<&mut MutableBTreeMapData<char, LetterData>>,
265) {
266    let current_map = get_active_map(&save_states, *active_save);
267    let map = HashMap::from([
268        (KeyCode::KeyA, 'a'),
269        (KeyCode::KeyB, 'b'),
270        (KeyCode::KeyC, 'c'),
271        (KeyCode::KeyD, 'd'),
272        (KeyCode::KeyE, 'e'),
273        (KeyCode::KeyF, 'f'),
274        (KeyCode::KeyG, 'g'),
275        (KeyCode::KeyH, 'h'),
276        (KeyCode::KeyI, 'i'),
277        (KeyCode::KeyJ, 'j'),
278        (KeyCode::KeyK, 'k'),
279        (KeyCode::KeyL, 'l'),
280        (KeyCode::KeyM, 'm'),
281        (KeyCode::KeyN, 'n'),
282        (KeyCode::KeyO, 'o'),
283        (KeyCode::KeyP, 'p'),
284        (KeyCode::KeyQ, 'q'),
285        (KeyCode::KeyR, 'r'),
286        (KeyCode::KeyS, 's'),
287        (KeyCode::KeyT, 't'),
288        (KeyCode::KeyU, 'u'),
289        (KeyCode::KeyV, 'v'),
290        (KeyCode::KeyW, 'w'),
291        (KeyCode::KeyX, 'x'),
292        (KeyCode::KeyY, 'y'),
293        (KeyCode::KeyZ, 'z'),
294    ]);
295    for (key, char) in map.iter() {
296        if keys.just_pressed(*key) {
297            let mut guard = current_map.write(&mut mutable_btree_map_datas);
298            guard.insert(
299                *char,
300                LetterData {
301                    pressed: true,
302                    count: guard.get(char).unwrap().count + 1,
303                },
304            );
305        } else if keys.just_released(*key) {
306            let mut guard = current_map.write(&mut mutable_btree_map_datas);
307            guard.insert(
308                *char,
309                LetterData {
310                    pressed: false,
311                    ..guard.get(char).unwrap().clone()
312                },
313            );
314        }
315    }
316}
317
318fn camera(mut commands: Commands) {
319    commands.spawn(Camera2d);
320}