Skip to main content

directional_navigation_overrides/
directional_navigation_overrides.rs

1//! Demonstrates automatic directional navigation with manual navigation overrides.
2//!
3//! This example shows how to leverage both automatic navigation and manual overrides to create
4//! a desired user navigation experience without much boilerplate code. In this example, there are
5//! multiple pages of UI Buttons that depict different scenarios in which both automatic and manual
6//! navigation are leveraged to produce a desired navigation experience.
7//!
8//! Manual overrides can be used to define navigation in any situation where automatic
9//! navigation fails to create an edge due to lack of proximity. For example, when creating
10//! navigation that loops around to an opposite side, manual overrides should be used to define
11//! this behavior. If one input is too far away from the others and `AutoNavigationConfig`
12//! cannot be tweaked, manual overrides can connect that input to the others. Manual navigation
13//! can also be used to override any undesired automatic navigation.
14//!
15//! The [`AutoDirectionalNavigation`] component is used to create basic, intuitive navigation to UI
16//! elements within a page. Manual navigation edges are added to the [`DirectionalNavigationMap`]
17//! to create special navigation rules. The [`AutoDirectionalNavigator`] system parameter navigates
18//! using manual navigation rules/overrides first and automatic navigation second.
19
20use core::time::Duration;
21
22use bevy::{
23    camera::NormalizedRenderTarget,
24    input_focus::{
25        directional_navigation::{
26            AutoNavigationConfig, DirectionalNavigationMap, DirectionalNavigationPlugin,
27        },
28        FocusCause, InputFocus, InputFocusVisible,
29    },
30    math::{CompassOctant, Dir2},
31    picking::{
32        backend::HitData,
33        pointer::{Location, PointerId},
34    },
35    platform::collections::HashSet,
36    prelude::*,
37    ui::auto_directional_navigation::{AutoDirectionalNavigation, AutoDirectionalNavigator},
38};
39
40fn main() {
41    App::new()
42        // Input focus is not enabled by default, so we need to add the corresponding plugins
43        // The navigation system's resources are initialized by the DirectionalNavigationPlugin.
44        .add_plugins((DefaultPlugins, DirectionalNavigationPlugin))
45        // This resource is canonically used to track whether or not to render a focus indicator
46        // It starts as false, but we set it to true here as we would like to see the focus indicator
47        .insert_resource(InputFocusVisible(true))
48        // Configure auto-navigation behavior
49        .insert_resource(AutoNavigationConfig {
50            // Require at least 10% overlap in perpendicular axis for cardinal directions
51            min_alignment_factor: 0.1,
52            // Don't connect nodes more than 200 pixels apart between their closest edges
53            max_search_distance: Some(200.0),
54            // Prefer nodes that are well-aligned
55            prefer_aligned: true,
56        })
57        .init_resource::<ActionState>()
58        // For automatic navigation, UI entities will have the component `AutoDirectionalNavigation`
59        // and will be automatically connected by the navigation system.
60        // We will also add some new edges that the automatic navigation system
61        // cannot create by itself by inserting them into `DirectionalNavigationMap`
62        .add_systems(Startup, setup_paged_ui)
63        // Input is generally handled during PreUpdate
64        .add_systems(PreUpdate, (process_inputs, navigate).chain())
65        .add_systems(
66            Update,
67            (
68                highlight_focused_element,
69                interact_with_focused_button,
70                reset_button_after_interaction,
71                update_focus_display
72                    .run_if(|input_focus: Res<InputFocus>| input_focus.is_changed()),
73                update_key_display,
74            ),
75        )
76        .add_observer(universal_button_click_behavior)
77        .run();
78}
79
80const PAGE_1_NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_400;
81const PAGE_1_PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_500;
82const PAGE_1_FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::BLUE_50;
83
84const PAGE_2_NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::RED_400;
85const PAGE_2_PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::RED_500;
86const PAGE_2_FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::RED_50;
87
88const PAGE_3_NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::GREEN_400;
89const PAGE_3_PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::GREEN_500;
90const PAGE_3_FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::GREEN_50;
91
92const NORMAL_BUTTON_COLORS: [Srgba; 3] = [
93    PAGE_1_NORMAL_BUTTON,
94    PAGE_2_NORMAL_BUTTON,
95    PAGE_3_NORMAL_BUTTON,
96];
97const PRESSED_BUTTON_COLORS: [Srgba; 3] = [
98    PAGE_1_PRESSED_BUTTON,
99    PAGE_2_PRESSED_BUTTON,
100    PAGE_3_PRESSED_BUTTON,
101];
102const FOCUSED_BORDER_COLORS: [Srgba; 3] = [
103    PAGE_1_FOCUSED_BORDER,
104    PAGE_2_FOCUSED_BORDER,
105    PAGE_3_FOCUSED_BORDER,
106];
107
108/// Marker component for the text that displays the currently focused button
109#[derive(Component)]
110struct FocusDisplay;
111
112/// Marker component for the text that displays the last key pressed
113#[derive(Component)]
114struct KeyDisplay;
115
116/// Component that stores which page a button is on
117#[derive(Component)]
118struct Page(usize);
119
120// Observer for button clicks
121fn universal_button_click_behavior(
122    mut click: On<Pointer<Click>>,
123    mut button_query: Query<(&mut BackgroundColor, &Page, &mut ResetTimer)>,
124) {
125    let button_entity = click.entity;
126    if let Ok((mut color, page, mut reset_timer)) = button_query.get_mut(button_entity) {
127        color.0 = PRESSED_BUTTON_COLORS[page.0].into();
128        reset_timer.0 = Timer::from_seconds(0.3, TimerMode::Once);
129        click.propagate(false);
130    }
131}
132
133#[derive(Component, Default, Deref, DerefMut)]
134struct ResetTimer(Timer);
135
136fn reset_button_after_interaction(
137    time: Res<Time>,
138    mut query: Query<(&mut ResetTimer, &mut BackgroundColor, &Page)>,
139) {
140    for (mut reset_timer, mut color, page) in query.iter_mut() {
141        reset_timer.tick(time.delta());
142        if reset_timer.just_finished() {
143            color.0 = NORMAL_BUTTON_COLORS[page.0].into();
144        }
145    }
146}
147
148/// Spawn pages of buttons to demonstrate automatic and manual navigation.
149///
150/// This function creates three pages of buttons. All buttons have automatic navigation
151/// enabled by having the [`AutoDirectionalNavigation`] component.
152/// Manual navigation is specified with the [`DirectionalNavigationMap`].
153/// Page 1 has a simple grid of buttons where transitions between rows is defined using
154/// the [`DirectionalNavigationMap`].
155/// Page 2 has a cluster of buttons to the top left and a lonely button on the bottom right.
156/// Navigation between the cluster and the lonely button is defined using the
157/// [`DirectionalNavigationMap`].
158/// Page 3 has the same simple grid of buttons as page 1, but automatic navigation has been
159/// overridden in the vertical direction with the [`DirectionalNavigationMap`].
160fn setup_paged_ui(
161    mut commands: Commands,
162    mut manual_directional_nav_map: ResMut<DirectionalNavigationMap>,
163    mut input_focus: ResMut<InputFocus>,
164) {
165    commands.spawn(Camera2d);
166
167    // Create a full-screen background node
168    let root_node = commands
169        .spawn(Node {
170            width: percent(100),
171            height: percent(100),
172            ..default()
173        })
174        .id();
175
176    // Instructions
177    let instructions = commands
178        .spawn((
179            Text::new(
180                "Directional Navigation Overrides Demo\n\n\
181                 Use arrow keys or D-pad to navigate.\n\
182                 Press Enter or A button to interact.\n\n\
183                 Navigation on each page is a combination of \
184                 both automatic and manual navigation.",
185            ),
186            Node {
187                position_type: PositionType::Absolute,
188                left: px(20),
189                top: px(20),
190                width: px(280),
191                padding: UiRect::all(px(12)),
192                border_radius: BorderRadius::all(px(8)),
193                ..default()
194            },
195            BackgroundColor(Color::srgba(0.1, 0.1, 0.1, 0.8)),
196        ))
197        .id();
198    commands.entity(root_node).add_children(&[instructions]);
199
200    // Focus display - shows which button is currently focused
201    commands.spawn((
202        Text::new("Focused: None"),
203        FocusDisplay,
204        Node {
205            position_type: PositionType::Absolute,
206            left: px(20),
207            bottom: px(80),
208            width: px(280),
209            padding: UiRect::all(px(12)),
210            border_radius: BorderRadius::all(px(8)),
211            ..default()
212        },
213        BackgroundColor(Color::srgba(0.1, 0.5, 0.1, 0.8)),
214        TextFont {
215            font_size: FontSize::Px(20.0),
216            ..default()
217        },
218    ));
219
220    // Key display - shows the last key pressed
221    commands.spawn((
222        Text::new("Last Key: None"),
223        KeyDisplay,
224        Node {
225            position_type: PositionType::Absolute,
226            left: px(20),
227            bottom: px(20),
228            width: px(280),
229            padding: UiRect::all(px(12)),
230            border_radius: BorderRadius::all(px(8)),
231            ..default()
232        },
233        BackgroundColor(Color::srgba(0.5, 0.1, 0.5, 0.8)),
234        TextFont {
235            font_size: FontSize::Px(20.0),
236            ..default()
237        },
238    ));
239
240    // Setup the pages with buttons and helper text
241    let mut pages_entities = [
242        Vec::with_capacity(12),
243        Vec::with_capacity(12),
244        Vec::with_capacity(12),
245    ];
246    let mut text_entities = Vec::with_capacity(10);
247    for (page_num, page_button_entities) in pages_entities.iter_mut().enumerate() {
248        if page_num == 1 {
249            // the second page
250            setup_buttons_for_triangle_page(
251                &mut commands,
252                page_num,
253                (page_button_entities, &mut text_entities),
254            );
255        } else {
256            // the first and third pages are regular grids
257            setup_buttons_for_grid_page(
258                &mut commands,
259                page_num,
260                (page_button_entities, &mut text_entities),
261            );
262        }
263
264        // Only the first page is visible at setup.
265        let visibility = if page_num == 0 {
266            Visibility::Visible
267        } else {
268            Visibility::Hidden
269        };
270        let page = commands
271            .spawn((
272                Node {
273                    width: percent(100),
274                    height: percent(100),
275                    ..default()
276                },
277                visibility,
278            ))
279            .id();
280
281        commands
282            .entity(page)
283            .add_children(page_button_entities)
284            .add_children(&text_entities);
285
286        text_entities.clear();
287    }
288
289    // For Pages 1 and 3, add manual edges within the grid page for navigation between rows.
290    let entity_pairs = [
291        // the end of the first row should connect to the beginning of the second
292        ((0, 2), (1, 0)),
293        // the end of the second row should connect to the beginning of the third
294        ((1, 2), (2, 0)),
295        // the end of the third row should connect to the beginning of the fourth
296        ((2, 2), (3, 0)),
297    ];
298    for (page_num, page_entities) in pages_entities.iter().enumerate() {
299        // Skip Page 2; we are only adding these manual edges for the grid pages.
300        if page_num == 1 {
301            continue;
302        }
303        for ((entity_a_row, entity_a_col), (entity_b_row, entity_b_col)) in entity_pairs.iter() {
304            manual_directional_nav_map.add_symmetrical_edge(
305                page_entities[entity_a_row * 3 + entity_a_col],
306                page_entities[entity_b_row * 3 + entity_b_col],
307                CompassOctant::East,
308            );
309        }
310    }
311
312    // Add manual edges within the triangle page (Page 2) between buttons 3 and 4.
313    // The `AutoNavigationConfig` is set to our desired values, but automatic
314    // navigation does not connect Button 3 to Button 4, so we have to add
315    // this navigation manually.
316    manual_directional_nav_map.add_symmetrical_edge(
317        pages_entities[1][2],
318        pages_entities[1][3],
319        CompassOctant::East,
320    );
321    manual_directional_nav_map.add_symmetrical_edge(
322        pages_entities[1][2],
323        pages_entities[1][3],
324        CompassOctant::South,
325    );
326    manual_directional_nav_map.add_symmetrical_edge(
327        pages_entities[1][2],
328        pages_entities[1][3],
329        CompassOctant::SouthEast,
330    );
331    // Add one-way blocking within the first grid page (Page 1) for down nav.
332    for btn in &pages_entities[0] {
333        manual_directional_nav_map.block_edge(*btn, CompassOctant::South);
334        manual_directional_nav_map.block_edge(*btn, CompassOctant::North);
335    }
336
337    // For Page 3, we override the navigation North and South to be inverted.
338    let mut col_entities = Vec::with_capacity(4);
339    for col in 0..=2 {
340        for row in 0..=3 {
341            col_entities.push(pages_entities[2][row * 3 + col]);
342        }
343        manual_directional_nav_map.add_looping_edges(&col_entities, CompassOctant::North);
344        col_entities.clear();
345    }
346
347    // Add manual edges between pages.
348    // When navigating east (right) from the last button of page 1,
349    // go to the first button of page 2. This edge is symmetrical.
350    manual_directional_nav_map.add_symmetrical_edge(
351        pages_entities[0][11],
352        pages_entities[1][0],
353        CompassOctant::East,
354    );
355    // When navigating south (down) from the last button of page 2,
356    // go to the first button of page 3. This edge is NOT symmetrical.
357    // This means going north (up) from the first button of page 3 does
358    // NOT go to the last button of page 2.
359    manual_directional_nav_map.add_edge(
360        pages_entities[1][3],
361        pages_entities[2][0],
362        CompassOctant::South,
363    );
364    // When navigating west (left) from the first button of page 3,
365    // go back to the last button of page 2. This edge is NOT symmetrical.
366    manual_directional_nav_map.add_edge(
367        pages_entities[2][0],
368        pages_entities[1][3],
369        CompassOctant::West,
370    );
371    // When navigating east (right) from the last button of page 1,
372    // go to the first button of page 2. This edge is symmetrical.
373    manual_directional_nav_map.add_symmetrical_edge(
374        pages_entities[2][11],
375        pages_entities[0][0],
376        CompassOctant::East,
377    );
378
379    // Set initial focus
380    input_focus.set(pages_entities[0][0], FocusCause::Navigated);
381}
382
383/// Creates the buttons and text for a grid page and places the ids into their
384/// respective Vecs in `entities`.
385fn setup_buttons_for_grid_page(
386    commands: &mut Commands,
387    page_num: usize,
388    entities: (&mut Vec<Entity>, &mut Vec<Entity>),
389) {
390    let (page_button_entities, text_entities) = entities;
391
392    // Spawn buttons in a grid
393    // Auto-navigation will automatically configure navigation within rows.
394    let button_positions = [
395        // Row 0
396        [(450.0, 80.0), (650.0, 80.0), (850.0, 80.0)],
397        // Row 1
398        [(450.0, 215.0), (650.0, 215.0), (850.0, 215.0)],
399        // Row 2
400        [(450.0, 350.0), (650.0, 350.0), (850.0, 350.0)],
401        // Row 3
402        [(450.0, 485.0), (650.0, 485.0), (850.0, 485.0)],
403    ];
404    for (i, row) in button_positions.iter().enumerate() {
405        for (j, (left, top)) in row.iter().enumerate() {
406            let button_entity = spawn_auto_nav_button(
407                commands,
408                format!("Btn {}-{}", i + 1, j + 1),
409                left,
410                top,
411                page_num,
412            );
413            page_button_entities.push(button_entity);
414        }
415    }
416
417    // Text describing current page
418    let current_page_entity = spawn_small_text_node(
419        commands,
420        format!("Currently on Page {}", page_num + 1),
421        650,
422        20,
423        Justify::Center,
424    );
425    text_entities.push(current_page_entity);
426
427    // Text describing direction to go to the previous page, placed left of the top-left button.
428    let previous_page = if page_num == 0 { 3 } else { page_num };
429    let previous_page_entity = spawn_small_text_node(
430        commands,
431        format!("Page {} << ", previous_page),
432        310,
433        120,
434        Justify::Right,
435    );
436    text_entities.push(previous_page_entity);
437
438    // Text describing direction to go to the next page, placed right of the bottom-right button.
439    let next_page_entity = spawn_small_text_node(
440        commands,
441        format!(">> Page {}", (page_num + 1) % 3 + 1),
442        1000,
443        525,
444        Justify::Left,
445    );
446    text_entities.push(next_page_entity);
447
448    // Texts describing that moving right wraps to the next row.
449    let right_1 = spawn_small_text_node(commands, "> Btn 2-1".into(), 1000, 120, Justify::Left);
450    let right_2 = spawn_small_text_node(commands, "> Btn 3-1".into(), 1000, 255, Justify::Left);
451    let right_3 = spawn_small_text_node(commands, "> Btn 4-1".into(), 1000, 390, Justify::Left);
452    let left_1 = spawn_small_text_node(commands, "Btn 1-3 < ".into(), 310, 255, Justify::Right);
453    let left_2 = spawn_small_text_node(commands, "Btn 2-3 < ".into(), 310, 390, Justify::Right);
454    let left_3 = spawn_small_text_node(commands, "Btn 3-3 < ".into(), 310, 525, Justify::Right);
455    text_entities.push(right_1);
456    text_entities.push(right_2);
457    text_entities.push(right_3);
458    text_entities.push(left_1);
459    text_entities.push(left_2);
460    text_entities.push(left_3);
461
462    let text = match page_num {
463        // For the first page, add a notice about vertical navigation being blocked off
464        0 => Text::new(
465            "Vertical movements disabled on each button, but you can still navigate between rows by going off the left or right sides."
466        ),
467        // For the third page, add a notice about vertical navigation being inverted in the grid.
468        2 => Text::new(
469            "Vertical Navigation has been manually overridden to be inverted! \
470            ^ moves down, and v (down) moves up.",
471        ),
472        _ => Text::default()
473    };
474    let footer_info = commands
475        .spawn((
476            text,
477            Node {
478                position_type: PositionType::Absolute,
479                left: px(450),
480                top: px(600),
481                width: px(540),
482                padding: UiRect::all(px(12)),
483                ..default()
484            },
485            TextFont {
486                font_size: FontSize::Px(20.0),
487                ..default()
488            },
489        ))
490        .id();
491    text_entities.push(footer_info);
492}
493
494/// Creates the buttons and text for the triangle page (page 2) and places the ids into their
495/// respective Vecs in `entities`.
496fn setup_buttons_for_triangle_page(
497    commands: &mut Commands,
498    page_num: usize,
499    entities: (&mut Vec<Entity>, &mut Vec<Entity>),
500) {
501    let button_positions = [
502        (450.0, 80.0),   // top left
503        (700.0, 80.0),   // top right
504        (575.0, 215.0),  // middle
505        (1050.0, 350.0), // bottom right
506    ];
507    let (page_button_entities, text_entities) = entities;
508    for (i, (left, top)) in button_positions.iter().enumerate() {
509        let button_entity =
510            spawn_auto_nav_button(commands, format!("Btn {}", i + 1), left, top, page_num);
511        page_button_entities.push(button_entity);
512    }
513
514    // Text describing current page
515    let current_page_entity = spawn_small_text_node(
516        commands,
517        format!("Currently on Page {}", page_num + 1),
518        650,
519        20,
520        Justify::Center,
521    );
522    text_entities.push(current_page_entity);
523
524    // Text describing direction to go to the previous page, placed left of the top-left button.
525    let previous_page = if page_num == 0 { 3 } else { page_num };
526    let previous_page_entity = spawn_small_text_node(
527        commands,
528        format!("Page {} << ", previous_page),
529        310,
530        120,
531        Justify::Right,
532    );
533    text_entities.push(previous_page_entity);
534
535    // Direction to navigate from button 3 to button 4, placed below center button
536    let below_button_three_entity =
537        spawn_small_text_node(commands, "v\nBtn 4".into(), 575, 325, Justify::Center);
538    text_entities.push(below_button_three_entity);
539
540    // Direction to navigate from button 3 to button 4, placed right of center button
541    let right_of_button_three_entity =
542        spawn_small_text_node(commands, "> Btn 4".into(), 735, 255, Justify::Left);
543    text_entities.push(right_of_button_three_entity);
544
545    // Direction to navigate from button 4 to button 3, placed above bottom right button
546    let below_button_three_entity =
547        spawn_small_text_node(commands, "Btn 3\n^".into(), 1050, 300, Justify::Center);
548    text_entities.push(below_button_three_entity);
549
550    // Direction to navigate from button 4 to button 3, placed left of bottom right button
551    let right_of_button_three_entity =
552        spawn_small_text_node(commands, "Btn 3 < ".into(), 910, 390, Justify::Right);
553    text_entities.push(right_of_button_three_entity);
554
555    // Direction to go to the next page, placed bottom of the bottom-right button.
556    let next_page_entity = spawn_small_text_node(
557        commands,
558        format!("V\nV\nPage {}", (page_num + 1) % 3 + 1),
559        1050,
560        460,
561        Justify::Center,
562    );
563    text_entities.push(next_page_entity);
564}
565
566fn spawn_auto_nav_button(
567    commands: &mut Commands,
568    text: String,
569    left: &f64,
570    top: &f64,
571    page_num: usize,
572) -> Entity {
573    commands
574        .spawn((
575            Button,
576            Node {
577                position_type: PositionType::Absolute,
578                left: px(*left),
579                top: px(*top),
580                width: px(140),
581                height: px(100),
582                border: UiRect::all(px(4)),
583                justify_content: JustifyContent::Center,
584                align_items: AlignItems::Center,
585                border_radius: BorderRadius::all(px(12)),
586                ..default()
587            },
588            Page(page_num),
589            BackgroundColor(NORMAL_BUTTON_COLORS[page_num].into()),
590            // Just add this component for automatic navigation
591            AutoDirectionalNavigation::default(),
592            ResetTimer::default(),
593            Name::new(text.clone()),
594        ))
595        .with_child((
596            Text::new(text),
597            TextLayout {
598                justify: Justify::Center,
599                ..default()
600            },
601        ))
602        .id()
603}
604
605fn spawn_small_text_node(
606    commands: &mut Commands,
607    text: String,
608    left: i32,
609    top: i32,
610    justify: Justify,
611) -> Entity {
612    commands
613        .spawn((
614            Text::new(text),
615            Node {
616                position_type: PositionType::Absolute,
617                left: px(left),
618                top: px(top),
619                width: px(140),
620                padding: UiRect::all(px(12)),
621                ..default()
622            },
623            TextFont {
624                font_size: FontSize::Px(20.0),
625                ..default()
626            },
627            TextLayout {
628                justify,
629                ..default()
630            },
631        ))
632        .id()
633}
634
635// Action state and input handling
636#[derive(Debug, PartialEq, Eq, Hash)]
637enum DirectionalNavigationAction {
638    Up,
639    Down,
640    Left,
641    Right,
642    Select,
643}
644
645impl DirectionalNavigationAction {
646    fn variants() -> Vec<Self> {
647        vec![
648            DirectionalNavigationAction::Up,
649            DirectionalNavigationAction::Down,
650            DirectionalNavigationAction::Left,
651            DirectionalNavigationAction::Right,
652            DirectionalNavigationAction::Select,
653        ]
654    }
655
656    fn keycode(&self) -> KeyCode {
657        match self {
658            DirectionalNavigationAction::Up => KeyCode::ArrowUp,
659            DirectionalNavigationAction::Down => KeyCode::ArrowDown,
660            DirectionalNavigationAction::Left => KeyCode::ArrowLeft,
661            DirectionalNavigationAction::Right => KeyCode::ArrowRight,
662            DirectionalNavigationAction::Select => KeyCode::Enter,
663        }
664    }
665
666    fn gamepad_button(&self) -> GamepadButton {
667        match self {
668            DirectionalNavigationAction::Up => GamepadButton::DPadUp,
669            DirectionalNavigationAction::Down => GamepadButton::DPadDown,
670            DirectionalNavigationAction::Left => GamepadButton::DPadLeft,
671            DirectionalNavigationAction::Right => GamepadButton::DPadRight,
672            DirectionalNavigationAction::Select => GamepadButton::South,
673        }
674    }
675}
676
677#[derive(Default, Resource)]
678struct ActionState {
679    pressed_actions: HashSet<DirectionalNavigationAction>,
680}
681
682fn process_inputs(
683    mut action_state: ResMut<ActionState>,
684    keyboard_input: Res<ButtonInput<KeyCode>>,
685    gamepad_input: Query<&Gamepad>,
686) {
687    action_state.pressed_actions.clear();
688
689    for action in DirectionalNavigationAction::variants() {
690        if keyboard_input.just_pressed(action.keycode()) {
691            action_state.pressed_actions.insert(action);
692        }
693    }
694
695    for gamepad in gamepad_input.iter() {
696        for action in DirectionalNavigationAction::variants() {
697            if gamepad.just_pressed(action.gamepad_button()) {
698                action_state.pressed_actions.insert(action);
699            }
700        }
701    }
702}
703
704fn navigate(
705    action_state: Res<ActionState>,
706    parent_query: Query<&ChildOf>,
707    mut visibility_query: Query<&mut Visibility>,
708    mut auto_directional_navigator: AutoDirectionalNavigator,
709) {
710    let net_east_west = action_state
711        .pressed_actions
712        .contains(&DirectionalNavigationAction::Right) as i8
713        - action_state
714            .pressed_actions
715            .contains(&DirectionalNavigationAction::Left) as i8;
716
717    let net_north_south = action_state
718        .pressed_actions
719        .contains(&DirectionalNavigationAction::Up) as i8
720        - action_state
721            .pressed_actions
722            .contains(&DirectionalNavigationAction::Down) as i8;
723
724    // Use Dir2::from_xy to convert input to direction, then convert to CompassOctant
725    let maybe_direction = Dir2::from_xy(net_east_west as f32, net_north_south as f32)
726        .ok()
727        .map(CompassOctant::from);
728
729    // Store the previous focus in case navigation switches pages.
730    let previous_focus = auto_directional_navigator.input_focus();
731    if let Some(direction) = maybe_direction {
732        match auto_directional_navigator.navigate(direction) {
733            Ok(new_focus) => {
734                // Successfully navigated!
735
736                // If navigation switches between pages, change the visibilities of pages
737                if let Ok(current_child_of) = parent_query.get(new_focus)
738                    && let Ok(mut current_page_visibility) =
739                        visibility_query.get_mut(current_child_of.parent())
740                {
741                    *current_page_visibility = Visibility::Visible;
742
743                    if let Some(previous_focus_entity) = previous_focus
744                        && let Ok(previous_child_of) = parent_query.get(previous_focus_entity)
745                        && previous_child_of.parent() != current_child_of.parent()
746                        && let Ok(mut previous_page_visibility) =
747                            visibility_query.get_mut(previous_child_of.parent())
748                    {
749                        *previous_page_visibility = Visibility::Hidden;
750                    }
751                }
752            }
753            Err(_e) => {
754                // Navigation failed (no neighbor in that direction)
755            }
756        }
757    }
758}
759
760fn update_focus_display(
761    input_focus: Res<InputFocus>,
762    button_query: Query<&Name, With<Button>>,
763    mut display_query: Query<&mut Text, With<FocusDisplay>>,
764) {
765    if let Ok(mut text) = display_query.single_mut() {
766        if let Some(focused_entity) = input_focus.get() {
767            if let Ok(name) = button_query.get(focused_entity) {
768                **text = format!("Focused: {}", name);
769            } else {
770                **text = "Focused: Unknown".to_string();
771            }
772        } else {
773            **text = "Focused: None".to_string();
774        }
775    }
776}
777
778fn update_key_display(
779    keyboard_input: Res<ButtonInput<KeyCode>>,
780    gamepad_input: Query<&Gamepad>,
781    mut display_query: Query<&mut Text, With<KeyDisplay>>,
782) {
783    if let Ok(mut text) = display_query.single_mut() {
784        // Check for keyboard inputs
785        for action in DirectionalNavigationAction::variants() {
786            if keyboard_input.just_pressed(action.keycode()) {
787                let key_name = match action {
788                    DirectionalNavigationAction::Up => "Up Arrow",
789                    DirectionalNavigationAction::Down => "Down Arrow",
790                    DirectionalNavigationAction::Left => "Left Arrow",
791                    DirectionalNavigationAction::Right => "Right Arrow",
792                    DirectionalNavigationAction::Select => "Enter",
793                };
794                **text = format!("Last Key: {}", key_name);
795                return;
796            }
797        }
798
799        // Check for gamepad inputs
800        for gamepad in gamepad_input.iter() {
801            for action in DirectionalNavigationAction::variants() {
802                if gamepad.just_pressed(action.gamepad_button()) {
803                    let button_name = match action {
804                        DirectionalNavigationAction::Up => "D-Pad Up",
805                        DirectionalNavigationAction::Down => "D-Pad Down",
806                        DirectionalNavigationAction::Left => "D-Pad Left",
807                        DirectionalNavigationAction::Right => "D-Pad Right",
808                        DirectionalNavigationAction::Select => "A Button",
809                    };
810                    **text = format!("Last Key: {}", button_name);
811                    return;
812                }
813            }
814        }
815    }
816}
817
818fn highlight_focused_element(
819    input_focus: Res<InputFocus>,
820    input_focus_visible: Res<InputFocusVisible>,
821    mut query: Query<(Entity, &mut BorderColor, &Page)>,
822) {
823    for (entity, mut border_color, page) in query.iter_mut() {
824        if input_focus.get() == Some(entity) && input_focus_visible.0 {
825            *border_color = BorderColor::all(FOCUSED_BORDER_COLORS[page.0]);
826        } else {
827            *border_color = BorderColor::DEFAULT;
828        }
829    }
830}
831
832fn interact_with_focused_button(
833    action_state: Res<ActionState>,
834    input_focus: Res<InputFocus>,
835    mut commands: Commands,
836) {
837    if action_state
838        .pressed_actions
839        .contains(&DirectionalNavigationAction::Select)
840        && let Some(focused_entity) = input_focus.get()
841    {
842        commands.trigger(Pointer::new(
843            PointerId::Mouse,
844            Location {
845                target: NormalizedRenderTarget::None {
846                    width: 0,
847                    height: 0,
848                },
849                position: Vec2::ZERO,
850            },
851            Click {
852                button: PointerButton::Primary,
853                hit: HitData {
854                    camera: Entity::PLACEHOLDER,
855                    depth: 0.0,
856                    position: None,
857                    normal: None,
858                    extra: None,
859                },
860                count: 1,
861                duration: Duration::from_secs_f32(0.1),
862            },
863            focused_entity,
864        ));
865    }
866}