1mod 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#[derive(Default)]
68struct Button {
69 el: El<Node>,
70 selected: Mutable<bool>,
71 hovered: Mutable<bool>,
72}
73
74impl 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 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
205static 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 .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.; impl 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 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; 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 .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 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 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 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 .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 .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(); 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 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 .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 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}