main_menu/
main_menu.rs

1//! - Main menu with sub menus for audio and graphics.
2//! - Simple buttons for option selection.
3//! - Slider for volume.
4//! - Dropdown for graphics quality (low/medium/high).
5//! - Navigation possible with mouse, keyboard and controller.
6//!   - Mouse: Separate styles for hover and press.
7//!   - Keyboard/Controller: Separate styles for currently focused element.
8
9mod utils;
10use utils::*;
11
12use std::{convert::identity, fmt::Display, time::Duration};
13
14use bevy::prelude::*;
15use haalka::prelude::*;
16use strum::{Display, EnumIter, IntoEnumIterator};
17
18fn main() {
19    App::new()
20        .add_plugins(examples_plugin)
21        .add_systems(
22            Startup,
23            (
24                |world: &mut World| {
25                    ui_root().spawn(world);
26                },
27                camera,
28            ),
29        )
30        .add_systems(Update, (keyboard_menu_input_events, gamepad_menu_input_events))
31        .insert_resource(AUDIO_SETTINGS.clone())
32        .insert_resource(GRAPHICS_SETTINGS.clone())
33        .insert_resource(MISC_DEMO_SETTINGS.clone())
34        .insert_resource(FocusedEntity(Entity::PLACEHOLDER))
35        .insert_resource(MenuInputRateLimiter(Timer::from_seconds(
36            MENU_INPUT_RATE_LIMIT,
37            TimerMode::Repeating,
38        )))
39        .insert_resource(SliderRateLimiter(Timer::from_seconds(
40            SLIDER_RATE_LIMIT,
41            TimerMode::Repeating,
42        )))
43        .run();
44}
45
46const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
47const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
48const CLICKED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
49const TEXT_COLOR: Color = Color::srgb(0.9, 0.9, 0.9);
50const FONT_SIZE: f32 = 25.;
51const MAIN_MENU_SIDES: f32 = 300.;
52const SUB_MENU_HEIGHT: f32 = 700.;
53const SUB_MENU_WIDTH: f32 = 1200.;
54const BASE_PADDING: f32 = 10.;
55const DEFAULT_BUTTON_HEIGHT: f32 = 65.;
56const BASE_BORDER_WIDTH: f32 = 5.;
57const MENU_ITEM_HEIGHT: f32 = DEFAULT_BUTTON_HEIGHT + BASE_PADDING;
58const LIL_BABY_BUTTON_SIZE: f32 = 30.;
59
60#[derive(Clone, Copy, PartialEq, Display, EnumIter)]
61enum SubMenu {
62    Audio,
63    Graphics,
64}
65
66// core widget, pretty much every other widget uses the `Button`
67#[derive(Default)]
68struct Button {
69    el: El<Node>,
70    selected: Mutable<bool>,
71    hovered: Mutable<bool>,
72}
73
74// implementing `ElementWrapper` allows the struct to be passed directly to .child methods
75impl ElementWrapper for Button {
76    type EL = El<Node>;
77    fn element_mut(&mut self) -> &mut Self::EL {
78        &mut self.el
79    }
80}
81
82impl GlobalEventAware for Button {}
83impl PointerEventAware for Button {}
84
85impl Button {
86    fn new() -> Self {
87        let (selected, selected_signal) = Mutable::new_and_signal(false);
88        let (pressed, pressed_signal) = Mutable::new_and_signal(false);
89        let (hovered, hovered_signal) = Mutable::new_and_signal(false);
90        let selected_hovered_broadcaster = map_ref!(selected_signal, pressed_signal, hovered_signal => (*selected_signal || *pressed_signal, *hovered_signal)).broadcast();
91        let border_color_signal = {
92            selected_hovered_broadcaster
93                .signal()
94                .map(|(selected, hovered)| {
95                    if selected {
96                        bevy::color::palettes::basic::RED.into()
97                    } else if hovered {
98                        Color::WHITE
99                    } else {
100                        Color::BLACK
101                    }
102                })
103                .map(BorderColor)
104        };
105        let background_color_signal = {
106            selected_hovered_broadcaster
107                .signal()
108                .map(|(selected, hovered)| {
109                    if selected {
110                        CLICKED_BUTTON
111                    } else if hovered {
112                        HOVERED_BUTTON
113                    } else {
114                        NORMAL_BUTTON
115                    }
116                })
117                .map(BackgroundColor)
118        };
119        Self {
120            el: {
121                El::<Node>::new()
122                    .with_node(|mut node| {
123                        node.height = Val::Px(DEFAULT_BUTTON_HEIGHT);
124                        node.border = UiRect::all(Val::Px(BASE_BORDER_WIDTH));
125                    })
126                    .pressed_sync(pressed)
127                    .cursor(CursorIcon::System(SystemCursorIcon::Pointer))
128                    .align_content(Align::center())
129                    .hovered_sync(hovered.clone())
130                    .border_color_signal(border_color_signal)
131                    .background_color_signal(background_color_signal)
132            },
133            selected,
134            hovered,
135        }
136    }
137
138    fn body(mut self, body: impl Element) -> Self {
139        self.el = self.el.child(body);
140        self
141    }
142
143    fn selected_signal(mut self, selected_signal: impl Signal<Item = bool> + Send + 'static) -> Self {
144        // syncing mutables like this is a helpful pattern for externally controlling reactive state that
145        // has default widget-internal behavior; for example, all buttons are selected on press, but
146        // what if we want the selectedness to persist? simply add another mutable that gets flipped
147        // on click and then pass a signal of that to this method, which is exactly how the
148        // `Checkbox` widget is implemented
149        let syncer = spawn(sync(selected_signal, self.selected.clone()));
150        self.el = self.el.update_raw_el(|raw_el| raw_el.hold_tasks([syncer]));
151        self
152    }
153
154    fn hovered_signal(mut self, hovered_signal: impl Signal<Item = bool> + Send + 'static) -> Self {
155        let syncer = spawn(sync(hovered_signal, self.hovered.clone()));
156        self.el = self.el.update_raw_el(|raw_el| raw_el.hold_tasks([syncer]));
157        self
158    }
159}
160
161fn text_button(
162    text_signal: impl Signal<Item = String> + Send + 'static,
163    on_click: impl FnMut() + Send + Sync + 'static,
164) -> Button {
165    Button::new()
166        .body(
167            El::<Text>::new()
168                .text_font(TextFont::from_font_size(FONT_SIZE))
169                .text_signal(text_signal.map(Text)),
170        )
171        .on_click(on_click)
172        .update_raw_el(|raw_el| raw_el.with_component::<Node>(|mut node| node.width = Val::Px(200.)))
173}
174
175fn sub_menu_button(sub_menu: SubMenu) -> Button {
176    text_button(always(sub_menu.to_string()), move || {
177        SHOW_SUB_MENU.set_neq(Some(sub_menu))
178    })
179}
180
181fn menu_base(width: f32, height: f32, title: &str) -> Column<Node> {
182    Column::<Node>::new()
183        .with_node(move |mut node| {
184            node.border = UiRect::all(Val::Px(BASE_BORDER_WIDTH));
185            node.width = Val::Px(width);
186            node.height = Val::Px(height);
187        })
188        .border_color(BorderColor(Color::BLACK))
189        .background_color(BackgroundColor(NORMAL_BUTTON))
190        .item(
191            El::<Node>::new()
192                .with_node(|mut node| {
193                    node.height = Val::Px(MENU_ITEM_HEIGHT);
194                    node.padding = UiRect::all(Val::Px(BASE_PADDING * 2.));
195                })
196                .child(
197                    El::<Text>::new()
198                        .align(Align::new().top().left())
199                        .text_font(TextFont::from_font_size(FONT_SIZE))
200                        .text(Text::new(title)),
201                ),
202        )
203}
204
205// global ui state comes in super handy sometimes ...
206// here, we use a global to keep track of any dropdowns that are dropped down, passing it to
207// `only_one_up_flipper` to ensure only one is dropped down at a time; a mutable for this can be
208// managed more locally, but adds significant unwieldiness
209static DROPDOWN_SHOWING_OPTION: LazyLock<Mutable<Option<Mutable<bool>>>> = LazyLock::new(default);
210
211fn lil_baby_button() -> Button {
212    Button::new().update_raw_el(|raw_el| {
213        raw_el.with_component::<Node>(|mut node| {
214            node.width = Val::Px(LIL_BABY_BUTTON_SIZE);
215            node.height = Val::Px(LIL_BABY_BUTTON_SIZE);
216        })
217    })
218}
219
220trait Controllable: ElementWrapper
221where
222    Self: Sized + 'static,
223{
224    fn controlling(&self) -> &Mutable<bool>;
225
226    fn controlling_signal(mut self, controlling_signal: impl Signal<Item = bool> + Send + 'static) -> Self {
227        let syncer = spawn(sync(controlling_signal, self.controlling().clone()));
228        self = self.update_raw_el(|raw_el| raw_el.hold_tasks([syncer]));
229        self
230    }
231}
232
233struct Checkbox {
234    el: Button,
235    controlling: Mutable<bool>,
236}
237
238impl Checkbox {
239    fn new(checked: Mutable<bool>) -> Self {
240        let (controlling, controlling_signal) = Mutable::new_and_signal(false);
241        Self {
242            el: {
243                lil_baby_button()
244                    .apply(|element| focus_on_signal(element, controlling.signal()))
245                    .update_raw_el(clone!((checked) move |raw_el| {
246                        raw_el.on_event_disableable_signal::<MenuInput>(
247                            move |event| {
248                                match event {
249                                    MenuInput::Select => {
250                                        checked.set_neq(!checked.get());
251                                    },
252                                    MenuInput::Delete => {
253                                        checked.set(false);
254                                    },
255                                    _ => ()
256                                }
257                            },
258                            signal::not(controlling_signal),
259                        )
260                    }))
261                    .on_click(clone!((checked) move || flip(&checked)))
262                    .selected_signal(checked.signal())
263            },
264            controlling,
265        }
266    }
267}
268
269impl ElementWrapper for Checkbox {
270    type EL = Button;
271    fn element_mut(&mut self) -> &mut Self::EL {
272        &mut self.el
273    }
274}
275
276impl Controllable for Checkbox {
277    fn controlling(&self) -> &Mutable<bool> {
278        &self.controlling
279    }
280}
281
282#[derive(Clone, Copy, EnumIter, PartialEq, Display)]
283enum Quality {
284    Low,
285    Medium,
286    High,
287    Ultra,
288}
289
290struct RadioGroup {
291    el: Row<Node>,
292    controlling: Mutable<bool>,
293}
294
295impl RadioGroup {
296    fn new<T: Clone + PartialEq + Display + Send + Sync + 'static>(
297        options: MutableVec<T>,
298        selected: Mutable<Option<usize>>,
299    ) -> Self {
300        let (controlling, controlling_signal) = Mutable::new_and_signal(false);
301        Self {
302            el: {
303                Row::<Node>::new()
304                .apply(|element| focus_on_signal(element, controlling.signal()))
305                .update_raw_el(|raw_el| {
306                    raw_el.on_event_disableable_signal::<MenuInput>(
307                        clone!((options, selected) move |event| {
308                            match event {
309                                MenuInput::Left | MenuInput::Right => {
310                                    let selected_option = selected.lock_ref().as_ref().copied();
311                                    let (mut i, step) = {
312                                        if matches!(event, MenuInput::Left) {
313                                            (selected_option.unwrap_or(options.lock_ref().len() - 1) as isize, -1)
314                                        } else {
315                                            (selected_option.unwrap_or(0) as isize, 1)
316                                        }
317                                    };
318                                    if selected_option.is_some() {
319                                        i = (i + step + options.lock_ref().len() as isize) % options.lock_ref().len() as isize;
320                                    }
321                                    selected.set(Some(i as usize));
322                                },
323                                MenuInput::Delete => {
324                                    selected.take();
325                                },
326                                _ => ()
327                            }
328                        }),
329                        signal::not(controlling_signal)
330                    )
331                })
332                .items_signal_vec(
333                    options.signal_vec_cloned().enumerate()
334                    .map(clone!((selected) move |(i_option_mutable, option)| {
335                        text_button(
336                            always(option.to_string()),
337                            clone!((selected, i_option_mutable) move || {
338                                if selected.get() == i_option_mutable.get() {
339                                    selected.set(None);
340                                } else {
341                                    selected.set(i_option_mutable.get());
342                                }
343                            })
344                        )
345                        // the `Checkbox` just used a flippable `Mutable<bool>` to persist the selectedness, and we could
346                        // have done the same here, e.g. a separate `clicked: Mutable<bool>` for every text button, but then to
347                        // get exclusivity we would have iterate over the other `clicked` mutables and flip them; again, this
348                        // is a totally valid option, but it's more convenient in this case to centrally track selectedness
349                        // with a `Mutable<Option<usize>>` so we get exclusivity for free; also notice that the index from the
350                        // `.enumerate` is a mutable, this is because the options vec is also reactive, so the indicies of items
351                        // can change, so this solution isn't actually correct for dynamic options, but it's fine for this example
352                        .selected_signal(signal_eq(selected.signal_cloned(), i_option_mutable.signal()))
353                    }))
354                )
355            },
356            controlling,
357        }
358    }
359}
360
361impl ElementWrapper for RadioGroup {
362    type EL = Row<Node>;
363    fn element_mut(&mut self) -> &mut Self::EL {
364        &mut self.el
365    }
366}
367
368impl Controllable for RadioGroup {
369    fn controlling(&self) -> &Mutable<bool> {
370        &self.controlling
371    }
372}
373
374enum LeftRight {
375    Left,
376    Right,
377}
378
379fn arrow_text(direction: LeftRight) -> El<Text> {
380    El::<Text>::new()
381        .text_font(TextFont::from_font_size(FONT_SIZE))
382        .text(Text::new(match direction {
383            LeftRight::Left => "<",
384            LeftRight::Right => ">",
385        }))
386}
387
388struct IterableOptions {
389    el: Row<Node>,
390    controlling: Mutable<bool>,
391}
392
393const FLASH_MS: f32 = 50.; // TODO: address background/border color desyncing
394
395impl IterableOptions {
396    fn new<T: Clone + PartialEq + Display + Send + Sync + 'static>(
397        options: MutableVec<T>,
398        selected: Mutable<T>,
399    ) -> Self {
400        let (controlling, controlling_signal) = Mutable::new_and_signal(false);
401        let left_pressed = Mutable::new(false);
402        let right_pressed = Mutable::new(false);
403        Self {
404            el: {
405                Row::<Node>::new()
406                .apply(|element| focus_on_signal(element, controlling.signal()))
407                .update_raw_el(|raw_el| {
408                    // TODO: only allowing one flasher like this doesn't prevent desyncing either ...
409                    let left_flasher = Mutable::new(None);
410                    let right_flasher = Mutable::new(None);
411                    raw_el.on_event_disableable_signal::<MenuInput>(
412                        clone!((options, selected, left_pressed, right_pressed) move |event| {
413                            match event {
414                                MenuInput::Left | MenuInput::Right => {
415                                    let i_option = options.lock_ref().iter().position(|option| option == &*selected.lock_ref()).map(|i| i as isize);
416                                    if let Some(mut i) = i_option {
417                                        let step = {
418                                            (if matches!(event, MenuInput::Left) {
419                                                left_pressed.set(true);
420                                                left_flasher.set(Some(spawn(clone!((left_pressed) async move {
421                                                    sleep(Duration::from_millis(FLASH_MS as u64)).await;
422                                                    left_pressed.signal().wait_for(true).await;  // TODO: this doesn't prevent desyncing, could be lower level issue ...
423                                                    left_pressed.set(false);
424                                                }))));
425                                                -1
426                                            } else {
427                                                right_pressed.set(true);
428                                                right_flasher.set(Some(spawn(clone!((right_pressed) async move {
429                                                    sleep(Duration::from_millis(FLASH_MS as u64)).await;
430                                                    right_pressed.signal().wait_for(true).await;
431                                                    right_pressed.set(false);
432                                                }))));
433                                                1
434                                            })
435                                            as isize
436                                        };
437                                        i = (i + step + options.lock_ref().len() as isize) % options.lock_ref().len() as isize;
438                                        selected.set(options.lock_ref()[i as usize].clone());
439                                    }
440                                },
441                                _ => ()
442                            }
443                        }),
444                        signal::not(controlling_signal)
445                    )
446                })
447                .with_node(|mut node| node.column_gap = Val::Px(BASE_PADDING * 2.))
448                .item({
449                    lil_baby_button()
450                    .selected_signal(left_pressed.signal())
451                    .on_click(clone!((selected, options) move || {
452                        let options_lock = options.lock_ref();
453                        if let Some(i) = options_lock.iter().position(|option| option == &*selected.lock_ref()) {
454                            selected.set_neq(options_lock.iter().rev().cycle().nth(options_lock.len() - i).unwrap().clone());
455                        }
456                    }))
457                    .body(arrow_text(LeftRight::Left))
458                })
459                .item(
460                    El::<Text>::new()
461                    .text_font(TextFont::from_font_size(FONT_SIZE))
462                    .text_signal(selected.signal_ref(ToString::to_string).map(Text))
463                )
464                .item({
465                    lil_baby_button()
466                    .selected_signal(right_pressed.signal())
467                    .on_click(clone!((selected, options) move || {
468                        let options_lock = options.lock_ref();
469                        if let Some(i) = options_lock.iter().position(|option| option == &*selected.lock_ref()) {
470                            selected.set_neq(options_lock.iter().cycle().nth(i + 1).unwrap().clone());
471                        }
472                    }))
473                    .body(arrow_text(LeftRight::Right))
474                })
475            },
476            controlling,
477        }
478    }
479}
480
481impl ElementWrapper for IterableOptions {
482    type EL = Row<Node>;
483    fn element_mut(&mut self) -> &mut Self::EL {
484        &mut self.el
485    }
486}
487
488impl Controllable for IterableOptions {
489    fn controlling(&self) -> &Mutable<bool> {
490        &self.controlling
491    }
492}
493
494struct Slider {
495    el: Row<Node>,
496    controlling: Mutable<bool>,
497}
498
499impl Slider {
500    fn new(value: Mutable<f32>) -> Self {
501        let (controlling, controlling_signal) = Mutable::new_and_signal(false);
502        Self {
503            el: {
504                let slider_width = 400.;
505                let slider_padding = 5.;
506                let max = slider_width - slider_padding - LIL_BABY_BUTTON_SIZE - BASE_BORDER_WIDTH;
507                let left = Mutable::new(value.get() / 100. * max);
508                let value_setter = spawn(clone!((left, value) async move {
509                    left.signal().for_each_sync(|left| value.set_neq(left / max * 100.)).await;
510                }));
511                Row::<Node>::new()
512                    .update_raw_el(|raw_el| raw_el.insert(SliderTag))
513                    .apply(|element| focus_on_signal(element, controlling.signal()))
514                    .update_raw_el(|raw_el| {
515                        raw_el.on_event_disableable_signal::<MenuInput>(
516                            clone!((left) move |event| {
517                                match event {
518                                    MenuInput::Left | MenuInput::Right => {
519                                        let dir = if matches!(event, MenuInput::Left) { -1. } else { 1. };
520                                        left.update(move |left| (left + dir * max * 0.001).max(0.).min(max));
521                                    },
522                                    _ => ()
523                                }
524                            }),
525                            signal::not(controlling_signal),
526                        )
527                    })
528                    .update_raw_el(|raw_el| raw_el.hold_tasks([value_setter]))
529                    .with_node(|mut node| node.column_gap = Val::Px(10.))
530                    .item(
531                        El::<Text>::new()
532                            .text_font(TextFont::from_font_size(FONT_SIZE))
533                            .text_signal(value.signal().map(|value| Text(format!("{value:.1}")))),
534                    )
535                    .item(
536                        Stack::<Node>::new()
537                            .with_node(move |mut node| {
538                                node.width = Val::Px(slider_width);
539                                node.height = Val::Px(5.);
540                                node.padding = UiRect::horizontal(Val::Px(slider_padding));
541                            })
542                            .background_color(BackgroundColor(Color::BLACK))
543                            .layer({
544                                let dragging = Mutable::new(false);
545                                lil_baby_button()
546                                    .selected_signal(dragging.signal())
547                                    .el // we need lower level access now
548                                    .on_signal_with_node(left.signal(), |mut node, left| node.left = Val::Px(left))
549                                    .align(Align::new().center_y())
550                                    .update_raw_el(|raw_el| {
551                                        raw_el
552                                            .on_event::<Pointer<DragStart>>(
553                                                clone!((dragging) move |_| dragging.set_neq(true)),
554                                            )
555                                            .on_event::<Pointer<DragEnd>>(move |_| dragging.set_neq(false))
556                                            .on_event::<Pointer<Drag>>(move |drag| {
557                                                left.set_neq((left.get() + drag.delta.x).max(0.).min(max));
558                                            })
559                                    })
560                            }),
561                    )
562            },
563            controlling,
564        }
565    }
566}
567
568impl ElementWrapper for Slider {
569    type EL = Row<Node>;
570    fn element_mut(&mut self) -> &mut Self::EL {
571        &mut self.el
572    }
573}
574
575impl Controllable for Slider {
576    fn controlling(&self) -> &Mutable<bool> {
577        &self.controlling
578    }
579}
580
581fn options(n: usize) -> Vec<String> {
582    (1..=n).map(|i| format!("option {i}")).collect()
583}
584
585fn only_one_up_flipper(
586    to_flip: &Mutable<bool>,
587    already_up_option: &Mutable<Option<Mutable<bool>>>,
588    target_option: Option<bool>,
589) {
590    let cur = target_option.map(|target| !target).unwrap_or(to_flip.get());
591    if cur {
592        already_up_option.take();
593    } else {
594        if let Some(previous) = &*already_up_option.lock_ref() {
595            previous.set(false);
596        }
597        already_up_option.set(Some(to_flip.clone()));
598    }
599    to_flip.set(!cur);
600}
601
602static MENU_ITEM_HOVERED_OPTION: LazyLock<Mutable<Option<Mutable<bool>>>> = LazyLock::new(default);
603
604fn menu_item(label: &str, body: impl Element, hovered: Mutable<bool>) -> Stack<Node> {
605    Stack::<Node>::new()
606        .background_color_signal(
607            hovered
608                .signal()
609                .map_bool(|| NORMAL_BUTTON.lighter(0.05), || NORMAL_BUTTON)
610                .map(BackgroundColor),
611        )
612        .on_hovered_change(move |is_hovered| only_one_up_flipper(&hovered, &MENU_ITEM_HOVERED_OPTION, Some(is_hovered)))
613        .with_node(|mut node| {
614            node.width = Val::Percent(100.);
615            node.height = Val::Px(MENU_ITEM_HEIGHT);
616            node.padding = UiRect::axes(Val::Px(BASE_PADDING), Val::Px(BASE_PADDING / 2.));
617        })
618        .layer(
619            El::<Text>::new()
620                .text_font(TextFont::from_font_size(FONT_SIZE))
621                .text(Text::new(label))
622                .align(Align::new().left().center_y()),
623        )
624        .layer(body.align(Align::new().right().center_y()))
625}
626
627struct Dropdown {
628    el: El<Node>,
629    controlling: Mutable<bool>,
630}
631
632fn focus_on_signal<E: Element>(element: E, signal: impl Signal<Item = bool> + Send + 'static) -> E {
633    element.update_raw_el(|raw_el| {
634        raw_el.on_signal(signal.dedupe(), |entity, focus| async move {
635            if focus {
636                // at first, i was using a `static_ref` global `Mutable<Option<Entity>>` for this
637                // and wrapping it in a resource for accessing it in the menu input event systems, but this is an
638                // anti pattern; the ecs should not be polling reactive ui state for syncing its own
639                // state/systems; instead, like we do here, simply use the `async_world` to update the ecs state
640                // *exactly and only* when it needs to be
641                async_world().insert_resource(FocusedEntity(entity)).await;
642            }
643        })
644    })
645}
646
647impl Dropdown {
648    fn new<T: Clone + PartialEq + Display + Send + Sync + 'static>(
649        options: MutableVec<T>,
650        selected: Mutable<Option<T>>,
651        clearable: bool,
652    ) -> Self {
653        let show_dropdown = Mutable::new(false);
654        let hovered = Mutable::new(false);
655        let controlling = Mutable::new(false);
656        let options_hovered =
657            MutableVec::new_with_values((0..options.lock_ref().len()).map(|_| Mutable::new(false)).collect());
658        let el = {
659            El::<Node>::new()
660            .apply(|element| focus_on_signal(element, controlling.signal()))
661            .update_raw_el(|raw_el| {
662                raw_el.observe::<MenuInput, _, _>(
663                    clone!((controlling, show_dropdown, hovered, options, options_hovered, selected) move |mut event: Trigger<MenuInput>| {
664                        // TODO: this is cringe, but the component driven alternative is equally cringe ? (need to use .observe here directly since we need to stop propagation conditionally within the body of the callback)
665                        if controlling.get() {
666                            match *event {
667                                MenuInput::Up | MenuInput::Down => {
668                                    if show_dropdown.get() {
669                                        event.propagate(false);
670                                        let hovered_option = options_hovered.lock_ref().iter().position(|hovered| hovered.get());
671                                        if let Some(i) = hovered_option {
672                                            options_hovered.lock_ref()[i].set(false);
673                                        }
674                                        let (mut i, step) = {
675                                            if matches!(*event, MenuInput::Up) {
676                                                (hovered_option.unwrap_or(options.lock_ref().len() - 1) as isize, -1)
677                                            } else {
678                                                (hovered_option.unwrap_or(0) as isize, 1)
679                                            }
680                                        };
681                                        if hovered_option.is_some() || (selected.lock_ref().is_some() && Some(&options.lock_ref()[i as usize]) == selected.lock_ref().as_ref()) {
682                                            for _ in 0..options.lock_ref().len() {
683                                                i = (i + step + options.lock_ref().len() as isize) % options.lock_ref().len() as isize;
684                                                if Some(&options.lock_ref()[i as usize]) != selected.lock_ref().as_ref() {
685                                                    break;
686                                                }
687                                            }
688                                        }
689                                        options_hovered.lock_ref()[i as usize].set(true);
690                                    } else {
691                                        hovered.set_neq(false);
692                                    }
693                                }
694                                MenuInput::Select => {
695                                    hovered.set_neq(!show_dropdown.get());
696                                    let hovered_option = options_hovered.lock_ref().iter().position(|hovered| hovered.get());
697                                    if let Some(i) = hovered_option {
698                                        options_hovered.lock_ref()[i].set(false);
699                                        selected.set_neq(Some(options.lock_ref()[i].clone()));
700                                    }
701                                    flip(&show_dropdown);
702                                    for hovered in options_hovered.lock_ref().iter() {
703                                        hovered.set(false);
704                                    }
705                                },
706                                MenuInput::Back => {
707                                    if show_dropdown.get() {
708                                        event.propagate(false);
709                                        for hovering in options_hovered.lock_ref().iter() {
710                                            hovering.set(false);
711                                        }
712                                        flip(&show_dropdown);
713                                    }
714                                    hovered.set(false);
715                                },
716                                MenuInput::Delete => {
717                                    if clearable {
718                                        selected.take();
719                                    }
720                                },
721                                _ => ()
722                            }
723                        }
724                    }),
725                )
726            })
727            .child(
728                Button::new()
729                .update_raw_el(|raw_el| raw_el.with_component::<Node>(|mut node| {
730                    node.width = Val::Px(300.)
731                }))
732                .hovered_signal(hovered.signal())
733                .body(
734                    Stack::<Node>::new()
735                    .with_node(|mut node| {
736                        node.width = Val::Percent(100.);
737                        node.padding = UiRect::horizontal(Val::Px(BASE_PADDING));
738                    })
739                    .layer(
740                        El::<Text>::new()
741                        .align(Align::new().left())
742                        .text_font(TextFont::from_font_size(FONT_SIZE))
743                        .text_signal(
744                            selected.signal_cloned()
745                            .map(|selected_option| {
746                                selected_option.map(|option| option.to_string()).unwrap_or_default()
747                            })
748                            .map(Text)
749                        )
750                    )
751                    .layer(
752                        Row::<Node>::new()
753                        .with_node(|mut node| node.column_gap = Val::Px(BASE_PADDING))
754                        .align(Align::new().right())
755                        .item_signal(
756                            // TODO: this should just work, but compiler asks for type info
757                            // clearable.then(||
758                            //     selected.signal_ref(Option::is_some).dedupe()
759                            //     .map_true(clone!((selected) move || x_button(clone!((selected) move || { selected.take(); }))))
760                            // )
761                            if clearable {
762                                selected.signal_ref(Option::is_some).dedupe()
763                                .map_true(clone!((selected) move || x_button(clone!((selected) move || { selected.take(); }))))
764                                .boxed()
765                            } else {
766                                always(None).boxed()
767                            }
768                        )
769                        .item(
770                            El::<Text>::new()
771                            .text_font(TextFont::from_font_size(FONT_SIZE))
772                            // TODO: need to figure out to rotate in place (around center)
773                            // .on_signal_with_transform(show_dropdown.signal(), |transform, showing| {
774                            //     transform.rotate_around(Vec3::X, Quat::from_rotation_z((if showing { 180.0f32 } else { 0. }).to_radians()));
775                            // })
776                            .text(Text::new("v"))
777                        )
778                    )
779                )
780                .on_click(clone!((show_dropdown) move || {
781                    only_one_up_flipper(&show_dropdown, &DROPDOWN_SHOWING_OPTION, None);
782                }))
783            )
784            // TODO: this should be element below signal
785            .child_signal(
786                show_dropdown.signal()
787                .map_true(clone!((options, show_dropdown, selected) move || {
788                    Column::<Node>::new()
789                    .with_node(|mut node| {
790                        node.width = Val::Percent(100.);
791                        node.position_type = PositionType::Absolute;
792                        node.top = Val::Percent(100.);
793                    })
794                    .items_signal_vec(
795                        options.signal_vec_cloned()
796                        .enumerate()
797                        .filter_signal_cloned(clone!((selected) move |(_, option)| {
798                            selected.signal_ref(clone!((option) move |selected_option| {
799                                selected_option.as_ref() != Some(&option)
800                            }))
801                            .dedupe()
802                        }))
803                        .map_signal(clone!((selected, show_dropdown, options_hovered) move |(i_mutable, option)| {
804                            i_mutable.signal()
805                            .map_some(clone!((options_hovered, selected, show_dropdown, option) move |i| {
806                                if let Some(hovered) = options_hovered.lock_ref().get(i) {
807                                    text_button(
808                                        always(option.to_string()),
809                                        clone!((selected, show_dropdown, option) move || {
810                                            selected.set_neq(Some(option.clone()));
811                                            flip(&show_dropdown);
812                                        })
813                                    )
814                                    .update_raw_el(|raw_el| raw_el.with_component::<Node>(|mut node| {
815                                        node.width = Val::Percent(100.)
816                                    }))
817                                    .hovered_signal(hovered.signal())
818                                    .apply(Some)
819                                } else {
820                                    None
821                                }
822                            }))
823                        }))
824                        .map(Option::flatten)
825                    )
826                }))
827            )
828        };
829        Self { el, controlling }
830    }
831}
832
833impl ElementWrapper for Dropdown {
834    type EL = El<Node>;
835    fn element_mut(&mut self) -> &mut Self::EL {
836        &mut self.el
837    }
838}
839
840impl Controllable for Dropdown {
841    fn controlling(&self) -> &Mutable<bool> {
842        &self.controlling
843    }
844}
845
846fn focus_on_no_child_hovered<E: Element>(
847    element: E,
848    hovereds: impl SignalVec<Item = Mutable<bool>> + Send + 'static,
849) -> E {
850    focus_on_signal(element, {
851        hovereds
852            .map_signal(|hovered| hovered.signal())
853            .to_signal_map(|is_hovereds| !is_hovereds.iter().copied().any(identity))
854            .dedupe()
855    })
856}
857
858fn sub_menu_child_hover_manager<E: Element>(element: E, hovereds: MutableVec<Mutable<bool>>) -> E {
859    let l = hovereds.lock_ref().len();
860    element.update_raw_el(|raw_el| {
861        raw_el.on_event::<MenuInput>(clone!((hovereds) move |event| {
862            let hovereds_lock = hovereds.lock_ref();
863            match event {
864                MenuInput::Up | MenuInput::Down => {
865                    let hovered_option = hovereds_lock.iter().position(|hovered| hovered.get());
866                    if let Some(i) = hovered_option {
867                        hovereds_lock[i].set(false);
868                        let new_i = if matches!(event, MenuInput::Up) { i + l - 1 } else { i + 1 } % l;
869                        hovereds_lock[new_i].set(true);
870                    } else {
871                        let i = if matches!(event, MenuInput::Up) { hovereds_lock.len() - 1 } else { 0 };
872                        hovereds_lock[i].set(true);
873                    }
874                },
875                MenuInput::Back => {
876                    if hovereds_lock.iter().any(|hovered| hovered.get()) {
877                        for hovered in hovereds_lock.iter() {
878                            hovered.set(false)
879                        }
880                    } else {
881                        SHOW_SUB_MENU.set(None);
882                    }
883                },
884                _ => ()
885            }
886        }))
887    })
888}
889
890fn make_controlling_menu_item(label: &str, el: impl Controllable + Element) -> (Stack<Node>, Mutable<bool>) {
891    let hovered = Mutable::new(false);
892    (
893        menu_item(label, el.controlling_signal(hovered.signal()), hovered.clone()),
894        hovered,
895    )
896}
897
898fn audio_menu() -> Column<Node> {
899    let items_hovereds = [
900        make_controlling_menu_item(
901            "dropdown",
902            Dropdown::new(
903                MutableVec::new_with_values(options(4)),
904                MISC_DEMO_SETTINGS.dropdown.clone(),
905                true,
906            ),
907        ),
908        make_controlling_menu_item(
909            "radio group",
910            RadioGroup::new(
911                MutableVec::new_with_values(options(3)),
912                MISC_DEMO_SETTINGS.radio_group.clone(),
913            ),
914        ),
915        make_controlling_menu_item("checkbox", Checkbox::new(MISC_DEMO_SETTINGS.checkbox.clone())),
916        make_controlling_menu_item(
917            "iterable options",
918            IterableOptions::new(
919                MutableVec::new_with_values(options(4)),
920                MISC_DEMO_SETTINGS.iterable_options.clone(),
921            ),
922        ),
923        make_controlling_menu_item("master volume", Slider::new(AUDIO_SETTINGS.master_volume.clone())),
924        make_controlling_menu_item("effect volume", Slider::new(AUDIO_SETTINGS.effect_volume.clone())),
925        make_controlling_menu_item("music volume", Slider::new(AUDIO_SETTINGS.music_volume.clone())),
926        make_controlling_menu_item("voice volume", Slider::new(AUDIO_SETTINGS.voice_volume.clone())),
927    ];
928    let l = items_hovereds.len();
929    let (items, hovereds): (Vec<_>, Vec<_>) = items_hovereds.into_iter().unzip();
930    let hovereds = MutableVec::new_with_values(hovereds);
931    menu_base(SUB_MENU_WIDTH, SUB_MENU_HEIGHT, "audio menu")
932        .apply(|element| focus_on_no_child_hovered(element, hovereds.signal_vec_cloned()))
933        .apply(|element| sub_menu_child_hover_manager(element, hovereds.clone()))
934        .items(
935            items
936                .into_iter()
937                .enumerate()
938                .map(move |(i, item)| item.z_index(ZIndex((l - i) as i32))),
939        )
940}
941
942fn graphics_menu() -> Column<Node> {
943    let preset_quality = GRAPHICS_SETTINGS.preset_quality.clone();
944    let texture_quality = GRAPHICS_SETTINGS.texture_quality.clone();
945    let shadow_quality = GRAPHICS_SETTINGS.shadow_quality.clone();
946    let bloom_quality = GRAPHICS_SETTINGS.bloom_quality.clone();
947    let non_preset_qualities = MutableVec::new_with_values(vec![
948        texture_quality.clone(),
949        shadow_quality.clone(),
950        bloom_quality.clone(),
951    ]);
952    let preset_broadcaster = spawn(clone!((preset_quality, non_preset_qualities) async move {
953        preset_quality.signal()
954        .for_each_sync(|preset_quality_option| {
955            if let Some(preset_quality) = preset_quality_option {
956                for quality in non_preset_qualities.lock_ref().iter() {
957                    quality.set_neq(Some(preset_quality));
958                }
959            }
960        })
961        .await;
962    }));
963    let preset_controller = spawn(clone!((preset_quality) async move {
964        non_preset_qualities.signal_vec_cloned()
965        .map_signal(|quality| quality.signal())
966        .to_signal_map(|qualities| {
967            let mut qualities = qualities.iter();
968            let mut preset = preset_quality.lock_mut();
969            if preset.is_none() {
970                let first = qualities.next().unwrap();  // always populated
971                if qualities.all(|quality| quality == first) {
972                    *preset = *first;
973                }
974            } else if preset.is_some() && qualities.any(|quality| quality != &*preset) {
975                *preset = None;
976            }
977        })
978        .to_future()
979        .await;
980    }));
981    let items = [
982        ("preset quality", preset_quality, true),
983        ("texture quality", texture_quality, false),
984        ("shadow quality", shadow_quality, false),
985        ("bloom quality", bloom_quality, false),
986    ];
987    let l = items.len();
988    let hovereds = MutableVec::new_with_values((0..l).map(|_| Mutable::new(false)).collect::<Vec<_>>());
989    menu_base(SUB_MENU_WIDTH, SUB_MENU_HEIGHT, "graphics menu")
990        .apply(|element| focus_on_no_child_hovered(element, hovereds.signal_vec_cloned()))
991        .apply(|element| sub_menu_child_hover_manager(element, hovereds.clone()))
992        .update_raw_el(|raw_el| raw_el.hold_tasks([preset_broadcaster, preset_controller]))
993        .items({
994            let hovereds = hovereds.lock_ref().iter().cloned().collect::<Vec<_>>();
995            items
996                .into_iter()
997                .zip(hovereds)
998                .enumerate()
999                .map(move |(i, ((label, quality, clearable), hovered))| {
1000                    menu_item(
1001                        label,
1002                        {
1003                            Dropdown::new(
1004                                MutableVec::new_with_values(Quality::iter().collect()),
1005                                quality,
1006                                clearable,
1007                            )
1008                            .controlling_signal(hovered.signal())
1009                        },
1010                        hovered,
1011                    )
1012                    .z_index(ZIndex((l - i) as i32))
1013                })
1014        })
1015        .item(
1016            // solely here to dehover dropdown menu items  // TODO: this can also be solved by
1017            // allowing setting Over/Out order at runtime or implementing .on_hovered_outside, i
1018            // should do both of these
1019            El::<Node>::new()
1020                .with_node(move |mut node| {
1021                    node.height = Val::Px(SUB_MENU_HEIGHT - (l + 1) as f32 * MENU_ITEM_HEIGHT - BASE_PADDING * 2.)
1022                })
1023                .on_hovered_change(|is_hovered| {
1024                    if is_hovered && let Some(hovered) = MENU_ITEM_HOVERED_OPTION.take() {
1025                        hovered.set(false);
1026                    }
1027                }),
1028        )
1029}
1030
1031fn x_button(on_click: impl FnMut() + Send + Sync + 'static) -> impl Element {
1032    let hovered = Mutable::new(false);
1033    El::<Node>::new()
1034        .background_color(BackgroundColor(Color::NONE))
1035        .hovered_sync(hovered.clone())
1036        // stop propagation because otherwise clearing the dropdown will drop down the
1037        // options too; the x should eat the click
1038        .on_click_stop_propagation(on_click)
1039        .child(
1040            El::<Text>::new()
1041                .text_font(TextFont::from_font_size(FONT_SIZE))
1042                .text(Text::new("x"))
1043                .text_color_signal(
1044                    hovered
1045                        .signal()
1046                        .map_bool(|| bevy::color::palettes::basic::RED.into(), || TEXT_COLOR)
1047                        .map(TextColor),
1048                ),
1049        )
1050}
1051
1052static SUB_MENU_SELECTED: LazyLock<Mutable<Option<SubMenu>>> = LazyLock::new(default);
1053
1054static SHOW_SUB_MENU: LazyLock<Mutable<Option<SubMenu>>> = LazyLock::new(default);
1055
1056fn menu() -> impl Element {
1057    Stack::<Node>::new()
1058        .layer(
1059            menu_base(MAIN_MENU_SIDES, MAIN_MENU_SIDES, "main menu")
1060                .apply(|element| focus_on_signal(element, SHOW_SUB_MENU.signal_ref(Option::is_none)))
1061                .update_raw_el(|raw_el| {
1062                    raw_el.on_event_disableable_signal::<MenuInput>(
1063                        move |event| match event {
1064                            MenuInput::Up | MenuInput::Down => {
1065                                if let Some(cur_sub_menu) = SUB_MENU_SELECTED.get() {
1066                                    if let Some(i) = SubMenu::iter().position(|sub_menu| cur_sub_menu == sub_menu) {
1067                                        let sub_menus = SubMenu::iter().collect::<Vec<_>>();
1068                                        SUB_MENU_SELECTED.set(if matches!(event, MenuInput::Down) {
1069                                            sub_menus.iter().rev().cycle().nth(sub_menus.len() - i).copied()
1070                                        } else {
1071                                            sub_menus.iter().cycle().nth(i + 1).copied()
1072                                        })
1073                                    }
1074                                } else {
1075                                    SUB_MENU_SELECTED.set_neq(Some(if matches!(event, MenuInput::Up) {
1076                                        SubMenu::iter().next_back().unwrap()
1077                                    } else {
1078                                        SubMenu::iter().next().unwrap()
1079                                    }));
1080                                }
1081                            }
1082                            MenuInput::Select => {
1083                                if let Some(sub_menu) = SUB_MENU_SELECTED.get() {
1084                                    SHOW_SUB_MENU.set_neq(Some(sub_menu));
1085                                }
1086                            }
1087                            MenuInput::Back => {
1088                                SUB_MENU_SELECTED.take();
1089                            }
1090                            _ => (),
1091                        },
1092                        SHOW_SUB_MENU.signal_ref(Option::is_some),
1093                    )
1094                })
1095                .with_node(|mut node| node.row_gap = Val::Px(BASE_PADDING * 2.))
1096                .item(
1097                    Column::<Node>::new()
1098                        .with_node(|mut node| node.row_gap = Val::Px(BASE_PADDING))
1099                        .align_content(Align::center())
1100                        .items(SubMenu::iter().map(|sub_menu| {
1101                            sub_menu_button(sub_menu).hovered_signal(
1102                                SUB_MENU_SELECTED.signal_ref(move |selected_option| selected_option == &Some(sub_menu)),
1103                            )
1104                        })),
1105                ),
1106        )
1107        .layer_signal(SHOW_SUB_MENU.signal().map_some(move |sub_menu| {
1108            let menu = match sub_menu {
1109                SubMenu::Audio => audio_menu(),
1110                SubMenu::Graphics => graphics_menu(),
1111            };
1112            Stack::<Node>::new()
1113                .with_node(|mut node| {
1114                    node.width = Val::Px(SUB_MENU_WIDTH);
1115                    node.height = Val::Px(SUB_MENU_HEIGHT);
1116                    // TODO: without absolute there's some weird bouncing when switching between
1117                    // menus, perhaps due to the layout system having to figure stuff out ?
1118                    node.position_type = PositionType::Absolute;
1119                })
1120                .align(Align::center())
1121                .layer(menu.align(Align::center()))
1122                .layer(
1123                    x_button(|| {
1124                        SHOW_SUB_MENU.take();
1125                    })
1126                    .align(Align::new().top().right())
1127                    .update_raw_el(|raw_el| {
1128                        raw_el.with_component::<Node>(|mut node| {
1129                            node.padding.right = Val::Px(BASE_PADDING);
1130                            node.padding.top = Val::Px(BASE_PADDING / 2.);
1131                        })
1132                    }),
1133                )
1134        }))
1135}
1136
1137fn camera(mut commands: Commands) {
1138    commands.spawn(Camera2d);
1139}
1140
1141#[derive(Resource, Clone)]
1142struct AudioSettings {
1143    master_volume: Mutable<f32>,
1144    effect_volume: Mutable<f32>,
1145    music_volume: Mutable<f32>,
1146    voice_volume: Mutable<f32>,
1147}
1148
1149static AUDIO_SETTINGS: LazyLock<AudioSettings> = LazyLock::new(|| AudioSettings {
1150    master_volume: Mutable::new(100.),
1151    effect_volume: Mutable::new(50.),
1152    music_volume: Mutable::new(50.),
1153    voice_volume: Mutable::new(50.),
1154});
1155
1156#[derive(Resource, Clone)]
1157struct GraphicsSettings {
1158    preset_quality: Mutable<Option<Quality>>,
1159    texture_quality: Mutable<Option<Quality>>,
1160    shadow_quality: Mutable<Option<Quality>>,
1161    bloom_quality: Mutable<Option<Quality>>,
1162}
1163
1164static GRAPHICS_SETTINGS: LazyLock<GraphicsSettings> = LazyLock::new(|| GraphicsSettings {
1165    preset_quality: Mutable::new(Some(Quality::Medium)),
1166    texture_quality: Mutable::new(Some(Quality::Medium)),
1167    shadow_quality: Mutable::new(Some(Quality::Medium)),
1168    bloom_quality: Mutable::new(Some(Quality::Medium)),
1169});
1170
1171#[derive(Resource, Clone)]
1172struct MiscDemoSettings {
1173    dropdown: Mutable<Option<String>>,
1174    radio_group: Mutable<Option<usize>>,
1175    checkbox: Mutable<bool>,
1176    iterable_options: Mutable<String>,
1177}
1178
1179static MISC_DEMO_SETTINGS: LazyLock<MiscDemoSettings> = LazyLock::new(|| MiscDemoSettings {
1180    dropdown: Mutable::new(None),
1181    radio_group: Mutable::new(None),
1182    checkbox: Mutable::new(false),
1183    iterable_options: Mutable::new("option 1".to_string()),
1184});
1185
1186#[derive(Clone, Copy, Component)]
1187enum MenuInput {
1188    Up,
1189    Down,
1190    Left,
1191    Right,
1192    Select,
1193    Back,
1194    Delete,
1195}
1196
1197impl Event for MenuInput {
1198    type Traversal = &'static ChildOf;
1199
1200    const AUTO_PROPAGATE: bool = true;
1201}
1202
1203#[derive(Resource)]
1204struct MenuInputRateLimiter(Timer);
1205
1206#[derive(Resource)]
1207struct SliderRateLimiter(Timer);
1208
1209enum PressedType {
1210    Pressed,
1211    JustPressed,
1212    Neither,
1213}
1214
1215fn rate_limited_menu_input(
1216    pressed_type: PressedType,
1217    input: MenuInput,
1218    entity: Entity,
1219    rate_limiter: &mut Timer,
1220    time: &Res<Time>,
1221    commands: &mut Commands,
1222) -> bool {
1223    match pressed_type {
1224        PressedType::Pressed => {
1225            if rate_limiter.tick(time.delta()).finished() {
1226                commands.trigger_targets(input, entity);
1227                rate_limiter.reset();
1228            }
1229            true
1230        }
1231        PressedType::JustPressed => {
1232            commands.trigger_targets(input, entity);
1233            rate_limiter.reset();
1234            true
1235        }
1236        PressedType::Neither => false,
1237    }
1238}
1239
1240#[derive(Component)]
1241struct SliderTag;
1242
1243fn keyboard_menu_input_events(
1244    sliders: Query<Entity, With<SliderTag>>,
1245    focused_entity: Res<FocusedEntity>,
1246    keys: Res<ButtonInput<KeyCode>>,
1247    mut menu_input_rate_limiter: ResMut<MenuInputRateLimiter>,
1248    mut slider_rate_limiter: ResMut<SliderRateLimiter>,
1249    time: Res<Time>,
1250    mut commands: Commands,
1251) {
1252    if keys.pressed(KeyCode::ShiftLeft) {
1253        let pressed_type = if keys.just_pressed(KeyCode::Tab) {
1254            PressedType::JustPressed
1255        } else if keys.pressed(KeyCode::Tab) {
1256            PressedType::Pressed
1257        } else {
1258            PressedType::Neither
1259        };
1260        let handled = rate_limited_menu_input(
1261            pressed_type,
1262            MenuInput::Up,
1263            focused_entity.0,
1264            &mut menu_input_rate_limiter.0,
1265            &time,
1266            &mut commands,
1267        );
1268        if handled {
1269            return;
1270        }
1271    }
1272    let slider_focused = sliders.get(focused_entity.0).is_ok();
1273    for (key, input) in [
1274        (KeyCode::ArrowUp, MenuInput::Up),
1275        (KeyCode::ArrowDown, MenuInput::Down),
1276        (KeyCode::ArrowLeft, MenuInput::Left),
1277        (KeyCode::ArrowRight, MenuInput::Right),
1278        (KeyCode::KeyW, MenuInput::Up),
1279        (KeyCode::KeyS, MenuInput::Down),
1280        (KeyCode::KeyA, MenuInput::Left),
1281        (KeyCode::KeyD, MenuInput::Right),
1282        (KeyCode::Enter, MenuInput::Select),
1283        (KeyCode::Escape, MenuInput::Back),
1284        (KeyCode::Backspace, MenuInput::Back),
1285        (KeyCode::Tab, MenuInput::Down),
1286        (KeyCode::Space, MenuInput::Select),
1287        (KeyCode::Delete, MenuInput::Delete),
1288    ] {
1289        let rate_limiter = {
1290            if slider_focused && matches!(input, MenuInput::Left | MenuInput::Right) {
1291                &mut slider_rate_limiter.0
1292            } else {
1293                &mut menu_input_rate_limiter.0
1294            }
1295        };
1296        let pressed_type = if keys.just_pressed(key) {
1297            PressedType::JustPressed
1298        } else if keys.pressed(key) {
1299            PressedType::Pressed
1300        } else {
1301            PressedType::Neither
1302        };
1303        rate_limited_menu_input(
1304            pressed_type,
1305            input,
1306            focused_entity.0,
1307            rate_limiter,
1308            &time,
1309            &mut commands,
1310        );
1311    }
1312}
1313
1314#[allow(clippy::too_many_arguments)]
1315fn gamepad_menu_input_events(
1316    sliders: Query<Entity, With<SliderTag>>,
1317    focused_entity: Res<FocusedEntity>,
1318    gamepads: Query<&Gamepad>,
1319    mut menu_input_rate_limiter: ResMut<MenuInputRateLimiter>,
1320    mut slider_rate_limiter: ResMut<SliderRateLimiter>,
1321    time: Res<Time>,
1322    mut commands: Commands,
1323) {
1324    let slider_focused = sliders.get(focused_entity.0).is_ok();
1325    for gamepad in gamepads.iter() {
1326        for (button, input) in [
1327            (GamepadButton::DPadUp, MenuInput::Up),
1328            (GamepadButton::DPadDown, MenuInput::Down),
1329            (GamepadButton::DPadLeft, MenuInput::Left),
1330            (GamepadButton::DPadRight, MenuInput::Right),
1331            (GamepadButton::North, MenuInput::Delete),
1332            (GamepadButton::South, MenuInput::Select),
1333            (GamepadButton::East, MenuInput::Back),
1334        ] {
1335            let rate_limiter = {
1336                if slider_focused && matches!(input, MenuInput::Left | MenuInput::Right) {
1337                    &mut slider_rate_limiter.0
1338                } else {
1339                    &mut menu_input_rate_limiter.0
1340                }
1341            };
1342            let pressed_type = if gamepad.pressed(button) {
1343                PressedType::Pressed
1344            } else if gamepad.just_pressed(button) {
1345                PressedType::JustPressed
1346            } else {
1347                PressedType::Neither
1348            };
1349            rate_limited_menu_input(
1350                pressed_type,
1351                input,
1352                focused_entity.0,
1353                rate_limiter,
1354                &time,
1355                &mut commands,
1356            );
1357        }
1358    }
1359}
1360
1361#[derive(Resource)]
1362struct FocusedEntity(Entity);
1363
1364const MENU_INPUT_RATE_LIMIT: f32 = 0.15;
1365const SLIDER_RATE_LIMIT: f32 = 0.001;
1366
1367fn ui_root() -> impl Element {
1368    El::<Node>::new()
1369        .with_node(|mut node| {
1370            node.width = Val::Percent(100.);
1371            node.height = Val::Percent(100.);
1372        })
1373        .cursor(CursorIcon::default())
1374        .align_content(Align::center())
1375        .child(menu())
1376}