1use core::time::Duration;
21
22use bevy::{
23 camera::NormalizedRenderTarget,
24 input_focus::{
25 directional_navigation::{
26 AutoNavigationConfig, DirectionalNavigationMap, DirectionalNavigationPlugin,
27 },
28 tab_navigation::TabIndex,
29 AutoFocus, InputFocus, InputFocusVisible,
30 },
31 math::{CompassOctant, Dir2},
32 picking::{
33 backend::HitData,
34 pointer::{Location, PointerId},
35 },
36 platform::collections::HashSet,
37 prelude::*,
38 ui::auto_directional_navigation::{AutoDirectionalNavigation, AutoDirectionalNavigator},
39 ui_widgets::Button,
40};
41
42fn main() {
43 App::new()
44 .add_plugins((DefaultPlugins, DirectionalNavigationPlugin))
47 .insert_resource(InputFocusVisible(true))
50 .insert_resource(AutoNavigationConfig {
52 min_alignment_factor: 0.1,
54 max_search_distance: Some(200.0),
56 prefer_aligned: true,
58 })
59 .init_resource::<ActionState>()
60 .add_systems(Startup, setup_paged_ui)
65 .add_systems(PreUpdate, (process_inputs, navigate).chain())
67 .add_systems(
68 Update,
69 (
70 highlight_focused_element,
71 interact_with_focused_button,
72 reset_button_after_interaction,
73 update_focus_display
74 .run_if(|input_focus: Res<InputFocus>| input_focus.is_changed()),
75 update_key_display,
76 ),
77 )
78 .add_observer(universal_button_click_behavior)
79 .run();
80}
81
82const PAGE_1_NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_400;
83const PAGE_1_PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_500;
84const PAGE_1_FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::BLUE_50;
85
86const PAGE_2_NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::RED_400;
87const PAGE_2_PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::RED_500;
88const PAGE_2_FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::RED_50;
89
90const PAGE_3_NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::GREEN_400;
91const PAGE_3_PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::GREEN_500;
92const PAGE_3_FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::GREEN_50;
93
94const NORMAL_BUTTON_COLORS: [Srgba; 3] = [
95 PAGE_1_NORMAL_BUTTON,
96 PAGE_2_NORMAL_BUTTON,
97 PAGE_3_NORMAL_BUTTON,
98];
99const PRESSED_BUTTON_COLORS: [Srgba; 3] = [
100 PAGE_1_PRESSED_BUTTON,
101 PAGE_2_PRESSED_BUTTON,
102 PAGE_3_PRESSED_BUTTON,
103];
104const FOCUSED_BORDER_COLORS: [Srgba; 3] = [
105 PAGE_1_FOCUSED_BORDER,
106 PAGE_2_FOCUSED_BORDER,
107 PAGE_3_FOCUSED_BORDER,
108];
109
110#[derive(Component, Clone, Copy, Default)]
112struct FocusDisplay;
113
114#[derive(Component, Clone, Copy, Default)]
116struct KeyDisplay;
117
118#[derive(Component, Clone, Copy, Default)]
120struct Page(usize);
121
122fn universal_button_click_behavior(
126 mut click: On<PointerClick>,
127 mut button_query: Query<(&mut BackgroundColor, &Page, &mut ResetTimer)>,
128 mut focus_visible: ResMut<InputFocusVisible>,
129) {
130 let button_entity = click.entity;
131 if let Ok((mut color, page, mut reset_timer)) = button_query.get_mut(button_entity) {
132 color.0 = PRESSED_BUTTON_COLORS[page.0].into();
133 reset_timer.0 = Timer::from_seconds(0.3, TimerMode::Once);
134 click.propagate(false);
135 focus_visible.0 = true;
138 }
139}
140
141#[derive(Component, Clone, Default, Deref, DerefMut)]
142struct ResetTimer(Timer);
143
144fn reset_button_after_interaction(
145 time: Res<Time>,
146 mut query: Query<(&mut ResetTimer, &mut BackgroundColor, &Page)>,
147) {
148 for (mut reset_timer, mut color, page) in query.iter_mut() {
149 reset_timer.tick(time.delta());
150 if reset_timer.just_finished() {
151 color.0 = NORMAL_BUTTON_COLORS[page.0].into();
152 }
153 }
154}
155
156fn setup_paged_ui(
169 mut commands: Commands,
170 mut manual_directional_nav_map: ResMut<DirectionalNavigationMap>,
171) {
172 commands.spawn(Camera2d);
173
174 commands.spawn_scene(bsn! {
176 Node {
177 width: percent(100),
178 height: percent(100),
179 }
180 Children [
181 @instructions_scene()
182 --
183 @focus_display_scene()
184 --
185 @key_display_scene()
186 ]
187 });
188
189 let pages_entities = [0, 1, 2]
191 .into_iter()
192 .map(|page_num| {
193 let button_entities = if page_num == 1 {
194 setup_buttons_for_triangle_page(&mut commands, page_num)
196 } else {
197 setup_buttons_for_grid_page(&mut commands, page_num)
199 };
200
201 let visibility = if page_num == 0 {
203 Visibility::Inherited
204 } else {
205 Visibility::Hidden
206 };
207
208 let page_id = if page_num == 1 {
210 commands
211 .spawn_scene(bsn! {
212 Node {
213 width: percent(100),
214 height: percent(100),
215 }
216 visibility
217 Children [
218 { triangle_page_text_entities_scene_list(page_num) }
219 ]
220 })
221 .id()
222 } else {
223 commands
224 .spawn_scene(bsn! {
225 Node {
226 width: percent(100),
227 height: percent(100),
228 }
229 visibility
230 Children [
231 { grid_page_text_entities_scene_list(page_num) }
232 ]
233 })
234 .id()
235 };
236
237 commands.entity(page_id).add_children(&button_entities);
238
239 button_entities
240 })
241 .collect::<Vec<_>>();
242
243 let entity_pairs = [
245 ((0, 2), (1, 0)),
247 ((1, 2), (2, 0)),
249 ((2, 2), (3, 0)),
251 ];
252 for (page_num, page_entities) in pages_entities.iter().enumerate() {
253 if page_num == 1 {
255 continue;
256 }
257 for ((entity_a_row, entity_a_col), (entity_b_row, entity_b_col)) in entity_pairs.iter() {
258 manual_directional_nav_map.add_symmetrical_edge(
259 page_entities[entity_a_row * 3 + entity_a_col],
260 page_entities[entity_b_row * 3 + entity_b_col],
261 CompassOctant::East,
262 );
263 }
264 }
265
266 manual_directional_nav_map.add_symmetrical_edge(
271 pages_entities[1][2],
272 pages_entities[1][3],
273 CompassOctant::East,
274 );
275 manual_directional_nav_map.add_symmetrical_edge(
276 pages_entities[1][2],
277 pages_entities[1][3],
278 CompassOctant::South,
279 );
280 manual_directional_nav_map.add_symmetrical_edge(
281 pages_entities[1][2],
282 pages_entities[1][3],
283 CompassOctant::SouthEast,
284 );
285 for btn in &pages_entities[0] {
287 manual_directional_nav_map.block_edge(*btn, CompassOctant::South);
288 manual_directional_nav_map.block_edge(*btn, CompassOctant::North);
289 }
290
291 let mut col_entities = Vec::with_capacity(4);
293 for col in 0..=2 {
294 for row in 0..=3 {
295 col_entities.push(pages_entities[2][row * 3 + col]);
296 }
297 manual_directional_nav_map.add_looping_edges(&col_entities, CompassOctant::North);
298 col_entities.clear();
299 }
300
301 manual_directional_nav_map.add_symmetrical_edge(
305 pages_entities[0][11],
306 pages_entities[1][0],
307 CompassOctant::East,
308 );
309 manual_directional_nav_map.add_edge(
314 pages_entities[1][3],
315 pages_entities[2][0],
316 CompassOctant::South,
317 );
318 manual_directional_nav_map.add_edge(
321 pages_entities[2][0],
322 pages_entities[1][3],
323 CompassOctant::West,
324 );
325 manual_directional_nav_map.add_symmetrical_edge(
328 pages_entities[2][11],
329 pages_entities[0][0],
330 CompassOctant::East,
331 );
332
333 commands.entity(pages_entities[0][0]).insert(AutoFocus);
335}
336
337fn instructions_scene() -> impl Scene {
339 bsn! {
340 Node {
341 position_type: PositionType::Absolute,
342 left: px(20),
343 top: px(20),
344 width: px(280),
345 padding: UiRect::all(px(12)),
346 border_radius: BorderRadius::all(px(8)),
347 }
348 BackgroundColor(Color::srgba(0.1, 0.1, 0.1, 0.8))
349 Children [
350 Text(
351 "Directional Navigation Overrides Demo\n\n\
352 Use arrow keys or D-pad to navigate.\n\
353 Press Enter or A button to interact.\n\n\
354 Navigation on each page is a combination of \
355 both automatic and manual navigation.",
356 )
357 ]
358 }
359}
360
361fn focus_display_scene() -> impl Scene {
363 bsn! {
364 Node {
365 position_type: PositionType::Absolute,
366 left: px(20),
367 bottom: px(80),
368 width: px(280),
369 padding: UiRect::all(px(12)),
370 border_radius: BorderRadius::all(px(8)),
371 }
372 BackgroundColor(Color::srgba(0.1, 0.5, 0.1, 0.8))
373 Children [
374 FocusDisplay
375 Text("Focused: None")
376 TextFont {
377 font_size: FontSize::Px(20.0),
378 }
379 ]
380 }
381}
382
383fn key_display_scene() -> impl Scene {
385 bsn! {
386 Node {
387 position_type: PositionType::Absolute,
388 left: px(20),
389 bottom: px(20),
390 width: px(280),
391 padding: UiRect::all(px(12)),
392 border_radius: BorderRadius::all(px(8)),
393 }
394 BackgroundColor(Color::srgba(0.5, 0.1, 0.5, 0.8))
395 Children [
396 KeyDisplay
397 Text("Last Key: None")
398 TextFont {
399 font_size: FontSize::Px(20.0)
400 }
401 ]
402 }
403}
404
405fn setup_buttons_for_grid_page(commands: &mut Commands, page_num: usize) -> Vec<Entity> {
409 let button_positions = [
412 [(450.0, 80.0), (650.0, 80.0), (850.0, 80.0)],
414 [(450.0, 215.0), (650.0, 215.0), (850.0, 215.0)],
416 [(450.0, 350.0), (650.0, 350.0), (850.0, 350.0)],
418 [(450.0, 485.0), (650.0, 485.0), (850.0, 485.0)],
420 ];
421 grid_auto_nav_buttons_scene(page_num, &button_positions)
422 .into_iter()
423 .map(|scene| commands.spawn_scene(scene).id())
424 .collect::<Vec<_>>()
425}
426
427fn grid_page_text_entities_scene_list(page_num: usize) -> impl SceneList {
429 let previous_page = if page_num == 0 { 3 } else { page_num };
430 let footer_text = match page_num {
431 0 => Text::new(
433 "Vertical movements disabled on each button, but you can still navigate between rows by going off the left or right sides."
434 ),
435 2 => Text::new(
437 "Vertical Navigation has been manually overridden to be inverted! \
438 ^ moves down, and v (down) moves up.",
439 ),
440 _ => Text::default()
441 };
442 bsn_list! {
443 @helper_text_node_scene(format!("Currently on Page {}", page_num + 1), 650, 10, Justify::Center)
445 --
446 @helper_text_node_scene(format!("Page {} << ", previous_page), 310, 120, Justify::Right)
448 --
449 @helper_text_node_scene(format!(">> Page {}", (page_num + 1) % 3 + 1), 1000, 525, Justify::Left)
451 --
452 @helper_text_node_scene("> Btn 2-1".into(), 1000, 120, Justify::Left)
454 --
455 @helper_text_node_scene("> Btn 3-1".into(), 1000, 255, Justify::Left)
456 --
457 @helper_text_node_scene("> Btn 4-1".into(), 1000, 390, Justify::Left)
458 --
459 @helper_text_node_scene("Btn 1-3 < ".into(), 310, 255, Justify::Right)
460 --
461 @helper_text_node_scene("Btn 2-3 < ".into(), 310, 390, Justify::Right)
462 --
463 @helper_text_node_scene("Btn 3-3 < ".into(), 310, 525, Justify::Right)
464 --
465 Node {
467 position_type: PositionType::Absolute,
468 left: px(450),
469 top: px(600),
470 width: px(540),
471 padding: UiRect::all(px(12)),
472 }
473 Children [
474 footer_text
475 TextFont {
476 font_size: FontSize::Px(20.0),
477 }
478 ]
479 }
480}
481
482fn setup_buttons_for_triangle_page(commands: &mut Commands, page_num: usize) -> Vec<Entity> {
486 let button_positions = [
487 (450.0, 80.0), (700.0, 80.0), (575.0, 215.0), (1050.0, 350.0), ];
492 button_positions
493 .iter()
494 .enumerate()
495 .map(|(i, (left, top))| {
496 auto_nav_button_scene(format!("Btn {}", i + 1), left, top, page_num)
497 })
498 .map(|scene| commands.spawn_scene(scene).id())
499 .collect::<Vec<_>>()
500}
501
502fn triangle_page_text_entities_scene_list(page_num: usize) -> impl SceneList {
504 let previous_page = if page_num == 0 { 3 } else { page_num };
505 bsn_list! {
506 @helper_text_node_scene(format!("Currently on Page {}", page_num + 1), 650, 20, Justify::Center)
508 --
509 @helper_text_node_scene(format!("Page {} << ", previous_page), 310, 120, Justify::Right)
511 --
512 @helper_text_node_scene("v\nBtn 4".into(), 575, 325, Justify::Center)
514 --
515 @helper_text_node_scene("> Btn 4".into(), 735, 255, Justify::Left)
517 --
518 @helper_text_node_scene("Btn 3\n^".into(), 1050, 300, Justify::Center)
520 --
521 @helper_text_node_scene("Btn 3 < ".into(), 910, 390, Justify::Right)
523 --
524 @helper_text_node_scene(format!("V\nV\nPage {}", (page_num + 1) % 3 + 1), 1050, 460, Justify::Center)
526 }
527}
528
529fn grid_auto_nav_buttons_scene(
532 page_num: usize,
533 button_positions: &[[(f64, f64); 3]; 4],
534) -> Vec<impl Scene> {
535 let mut scenes = vec![];
536 for (i, row) in button_positions.iter().enumerate() {
537 for (j, (left, top)) in row.iter().enumerate() {
538 scenes.push(auto_nav_button_scene(
539 format!("Btn {}-{}", i + 1, j + 1),
540 left,
541 top,
542 page_num,
543 ));
544 }
545 }
546 scenes
547}
548
549fn auto_nav_button_scene(text: String, left: &f64, top: &f64, page_num: usize) -> impl Scene {
551 let text_clone = text.clone();
552 bsn! {
553 Button
554 TabIndex(0)
557 Node {
558 position_type: PositionType::Absolute,
559 left: px(*left),
560 top: px(*top),
561 width: px(140),
562 height: px(100),
563 border: UiRect::all(px(4)),
564 justify_content: JustifyContent::Center,
565 align_items: AlignItems::Center,
566 border_radius: BorderRadius::all(px(12)),
567 }
568 Page(page_num)
569 BackgroundColor({NORMAL_BUTTON_COLORS[page_num]})
570 AutoDirectionalNavigation
572 ResetTimer
573 Name(text)
574 Children [
575 Text(text_clone)
576 TextLayout {
577 justify: Justify::Center,
578 }
579 ]
580 }
581}
582
583fn helper_text_node_scene(text: String, left: i32, top: i32, justify: Justify) -> impl Scene {
585 bsn! {
586 Node {
587 position_type: PositionType::Absolute,
588 left: px(left),
589 top: px(top),
590 width: px(140),
591 padding: UiRect::all(px(12)),
592 }
593 Children [
594 Text(text)
595 TextFont {
596 font_size: FontSize::Px(20.0),
597 }
598 TextLayout {
599 justify,
600 }
601 ]
602 }
603}
604
605#[derive(Debug, PartialEq, Eq, Hash)]
607enum DirectionalNavigationAction {
608 Up,
609 Down,
610 Left,
611 Right,
612 Select,
613}
614
615impl DirectionalNavigationAction {
616 fn variants() -> Vec<Self> {
617 vec![
618 DirectionalNavigationAction::Up,
619 DirectionalNavigationAction::Down,
620 DirectionalNavigationAction::Left,
621 DirectionalNavigationAction::Right,
622 DirectionalNavigationAction::Select,
623 ]
624 }
625
626 fn keycode(&self) -> KeyCode {
627 match self {
628 DirectionalNavigationAction::Up => KeyCode::ArrowUp,
629 DirectionalNavigationAction::Down => KeyCode::ArrowDown,
630 DirectionalNavigationAction::Left => KeyCode::ArrowLeft,
631 DirectionalNavigationAction::Right => KeyCode::ArrowRight,
632 DirectionalNavigationAction::Select => KeyCode::Enter,
633 }
634 }
635
636 fn gamepad_button(&self) -> GamepadButton {
637 match self {
638 DirectionalNavigationAction::Up => GamepadButton::DPadUp,
639 DirectionalNavigationAction::Down => GamepadButton::DPadDown,
640 DirectionalNavigationAction::Left => GamepadButton::DPadLeft,
641 DirectionalNavigationAction::Right => GamepadButton::DPadRight,
642 DirectionalNavigationAction::Select => GamepadButton::South,
643 }
644 }
645}
646
647#[derive(Default, Resource)]
648struct ActionState {
649 pressed_actions: HashSet<DirectionalNavigationAction>,
650}
651
652fn process_inputs(
653 mut action_state: ResMut<ActionState>,
654 keyboard_input: Res<ButtonInput<KeyCode>>,
655 gamepad_input: Query<&Gamepad>,
656) {
657 action_state.pressed_actions.clear();
658
659 for action in DirectionalNavigationAction::variants() {
660 if keyboard_input.just_pressed(action.keycode()) {
661 action_state.pressed_actions.insert(action);
662 }
663 }
664
665 for gamepad in gamepad_input.iter() {
666 for action in DirectionalNavigationAction::variants() {
667 if gamepad.just_pressed(action.gamepad_button()) {
668 action_state.pressed_actions.insert(action);
669 }
670 }
671 }
672}
673
674fn navigate(
675 action_state: Res<ActionState>,
676 parent_query: Query<&ChildOf>,
677 mut visibility_query: Query<&mut Visibility>,
678 mut auto_directional_navigator: AutoDirectionalNavigator,
679) {
680 let net_east_west = action_state
681 .pressed_actions
682 .contains(&DirectionalNavigationAction::Right) as i8
683 - action_state
684 .pressed_actions
685 .contains(&DirectionalNavigationAction::Left) as i8;
686
687 let net_north_south = action_state
688 .pressed_actions
689 .contains(&DirectionalNavigationAction::Up) as i8
690 - action_state
691 .pressed_actions
692 .contains(&DirectionalNavigationAction::Down) as i8;
693
694 let maybe_direction = Dir2::from_xy(net_east_west as f32, net_north_south as f32)
696 .ok()
697 .map(CompassOctant::from);
698
699 let previous_focus = auto_directional_navigator.input_focus();
701 if let Some(direction) = maybe_direction {
702 match auto_directional_navigator.navigate(direction) {
703 Ok(new_focus) => {
704 if let Ok(current_child_of) = parent_query.get(new_focus)
708 && let Ok(mut current_page_visibility) =
709 visibility_query.get_mut(current_child_of.parent())
710 {
711 *current_page_visibility = Visibility::Inherited;
712
713 if let Some(previous_focus_entity) = previous_focus
714 && let Ok(previous_child_of) = parent_query.get(previous_focus_entity)
715 && previous_child_of.parent() != current_child_of.parent()
716 && let Ok(mut previous_page_visibility) =
717 visibility_query.get_mut(previous_child_of.parent())
718 {
719 *previous_page_visibility = Visibility::Hidden;
720 }
721 }
722 }
723 Err(_e) => {
724 }
726 }
727 }
728}
729
730fn update_focus_display(
731 input_focus: Res<InputFocus>,
732 button_query: Query<&Name, With<Button>>,
733 mut display_query: Query<&mut Text, With<FocusDisplay>>,
734) {
735 if let Ok(mut text) = display_query.single_mut() {
736 if let Some(focused_entity) = input_focus.get() {
737 if let Ok(name) = button_query.get(focused_entity) {
738 **text = format!("Focused: {}", name);
739 } else {
740 **text = "Focused: Unknown".to_string();
741 }
742 } else {
743 **text = "Focused: None".to_string();
744 }
745 }
746}
747
748fn update_key_display(
749 keyboard_input: Res<ButtonInput<KeyCode>>,
750 gamepad_input: Query<&Gamepad>,
751 mut display_query: Query<&mut Text, With<KeyDisplay>>,
752) {
753 if let Ok(mut text) = display_query.single_mut() {
754 for action in DirectionalNavigationAction::variants() {
756 if keyboard_input.just_pressed(action.keycode()) {
757 let key_name = match action {
758 DirectionalNavigationAction::Up => "Up Arrow",
759 DirectionalNavigationAction::Down => "Down Arrow",
760 DirectionalNavigationAction::Left => "Left Arrow",
761 DirectionalNavigationAction::Right => "Right Arrow",
762 DirectionalNavigationAction::Select => "Enter",
763 };
764 **text = format!("Last Key: {}", key_name);
765 return;
766 }
767 }
768
769 for gamepad in gamepad_input.iter() {
771 for action in DirectionalNavigationAction::variants() {
772 if gamepad.just_pressed(action.gamepad_button()) {
773 let button_name = match action {
774 DirectionalNavigationAction::Up => "D-Pad Up",
775 DirectionalNavigationAction::Down => "D-Pad Down",
776 DirectionalNavigationAction::Left => "D-Pad Left",
777 DirectionalNavigationAction::Right => "D-Pad Right",
778 DirectionalNavigationAction::Select => "A Button",
779 };
780 **text = format!("Last Key: {}", button_name);
781 return;
782 }
783 }
784 }
785 }
786}
787
788fn highlight_focused_element(
789 input_focus: Res<InputFocus>,
790 input_focus_visible: Res<InputFocusVisible>,
791 mut query: Query<(Entity, &mut BorderColor, &Page)>,
792) {
793 for (entity, mut border_color, page) in query.iter_mut() {
794 if input_focus.get() == Some(entity) && input_focus_visible.0 {
795 *border_color = BorderColor::all(FOCUSED_BORDER_COLORS[page.0]);
796 } else {
797 *border_color = BorderColor::DEFAULT;
798 }
799 }
800}
801
802fn interact_with_focused_button(
803 action_state: Res<ActionState>,
804 input_focus: Res<InputFocus>,
805 mut commands: Commands,
806) {
807 if action_state
808 .pressed_actions
809 .contains(&DirectionalNavigationAction::Select)
810 && let Some(focused_entity) = input_focus.get()
811 {
812 commands.trigger(PointerClick {
813 entity: focused_entity,
814 pointer: Pointer::new(
815 PointerId::Mouse,
816 Location {
817 target: NormalizedRenderTarget::None {
818 width: 0,
819 height: 0,
820 },
821 position: Vec2::ZERO,
822 },
823 ),
824 button: PointerButton::Primary,
825 hit: HitData {
826 camera: Entity::PLACEHOLDER,
827 depth: 0.0,
828 position: None,
829 normal: None,
830 extra: None,
831 },
832 count: 1,
833 duration: Duration::from_secs_f32(0.1),
834 });
835 }
836}