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        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        // Input focus is not enabled by default, so we need to add the corresponding plugins
45        // The navigation system's resources are initialized by the DirectionalNavigationPlugin.
46        .add_plugins((DefaultPlugins, DirectionalNavigationPlugin))
47        // This resource is canonically used to track whether or not to render a focus indicator
48        // It starts as false, but we set it to true here as we would like to see the focus indicator
49        .insert_resource(InputFocusVisible(true))
50        // Configure auto-navigation behavior
51        .insert_resource(AutoNavigationConfig {
52            // Require at least 10% overlap in perpendicular axis for cardinal directions
53            min_alignment_factor: 0.1,
54            // Don't connect nodes more than 200 pixels apart between their closest edges
55            max_search_distance: Some(200.0),
56            // Prefer nodes that are well-aligned
57            prefer_aligned: true,
58        })
59        .init_resource::<ActionState>()
60        // For automatic navigation, UI entities will have the component `AutoDirectionalNavigation`
61        // and will be automatically connected by the navigation system.
62        // We will also add some new edges that the automatic navigation system
63        // cannot create by itself by inserting them into `DirectionalNavigationMap`
64        .add_systems(Startup, setup_paged_ui)
65        // Input is generally handled during PreUpdate
66        .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/// Marker component for the text that displays the currently focused button
111#[derive(Component, Clone, Copy, Default)]
112struct FocusDisplay;
113
114/// Marker component for the text that displays the last key pressed
115#[derive(Component, Clone, Copy, Default)]
116struct KeyDisplay;
117
118/// Component that stores which page a button is on
119#[derive(Component, Clone, Copy, Default)]
120struct Page(usize);
121
122/// Observer for button clicks. Clicking on a button is considered navigation to that button.
123/// This is also triggered when `interact_with_focused_button` simulates a button click with a
124/// manually sent pointer click event
125fn 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        // The focus should be visible because a button was clicked.
136        // It may have been set to false by the `PointerFocusPlugin`
137        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
156/// Spawn pages of buttons to demonstrate automatic and manual navigation.
157///
158/// This function creates three pages of buttons. All buttons have automatic navigation
159/// enabled by having the [`AutoDirectionalNavigation`] component.
160/// Manual navigation is specified with the [`DirectionalNavigationMap`].
161/// Page 1 has a simple grid of buttons where transitions between rows is defined using
162/// the [`DirectionalNavigationMap`].
163/// Page 2 has a cluster of buttons to the top left and a lonely button on the bottom right.
164/// Navigation between the cluster and the lonely button is defined using the
165/// [`DirectionalNavigationMap`].
166/// Page 3 has the same simple grid of buttons as page 1, but automatic navigation has been
167/// overridden in the vertical direction with the [`DirectionalNavigationMap`].
168fn setup_paged_ui(
169    mut commands: Commands,
170    mut manual_directional_nav_map: ResMut<DirectionalNavigationMap>,
171) {
172    commands.spawn(Camera2d);
173
174    // Create a full-screen background node
175    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    // Setup the pages with buttons and helper text
190    let pages_entities = [0, 1, 2]
191        .into_iter()
192        .map(|page_num| {
193            let button_entities = if page_num == 1 {
194                // the second page
195                setup_buttons_for_triangle_page(&mut commands, page_num)
196            } else {
197                // the first and third pages are regular grids
198                setup_buttons_for_grid_page(&mut commands, page_num)
199            };
200
201            // Only the first page is visible at setup.
202            let visibility = if page_num == 0 {
203                Visibility::Inherited
204            } else {
205                Visibility::Hidden
206            };
207
208            // Each page is its own separate root UI node with its own visibility.
209            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    // For Pages 1 and 3, add manual edges within the grid page for navigation between rows.
244    let entity_pairs = [
245        // the end of the first row should connect to the beginning of the second
246        ((0, 2), (1, 0)),
247        // the end of the second row should connect to the beginning of the third
248        ((1, 2), (2, 0)),
249        // the end of the third row should connect to the beginning of the fourth
250        ((2, 2), (3, 0)),
251    ];
252    for (page_num, page_entities) in pages_entities.iter().enumerate() {
253        // Skip Page 2; we are only adding these manual edges for the grid pages.
254        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    // Add manual edges within the triangle page (Page 2) between buttons 3 and 4.
267    // The `AutoNavigationConfig` is set to our desired values, but automatic
268    // navigation does not connect Button 3 to Button 4, so we have to add
269    // this navigation manually.
270    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    // Add one-way blocking within the first grid page (Page 1) for down nav.
286    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    // For Page 3, we override the navigation North and South to be inverted.
292    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    // Add manual edges between pages.
302    // When navigating east (right) from the last button of page 1,
303    // go to the first button of page 2. This edge is symmetrical.
304    manual_directional_nav_map.add_symmetrical_edge(
305        pages_entities[0][11],
306        pages_entities[1][0],
307        CompassOctant::East,
308    );
309    // When navigating south (down) from the last button of page 2,
310    // go to the first button of page 3. This edge is NOT symmetrical.
311    // This means going north (up) from the first button of page 3 does
312    // NOT go to the last button of page 2.
313    manual_directional_nav_map.add_edge(
314        pages_entities[1][3],
315        pages_entities[2][0],
316        CompassOctant::South,
317    );
318    // When navigating west (left) from the first button of page 3,
319    // go back to the last button of page 2. This edge is NOT symmetrical.
320    manual_directional_nav_map.add_edge(
321        pages_entities[2][0],
322        pages_entities[1][3],
323        CompassOctant::West,
324    );
325    // When navigating east (right) from the last button of page 1,
326    // go to the first button of page 2. This edge is symmetrical.
327    manual_directional_nav_map.add_symmetrical_edge(
328        pages_entities[2][11],
329        pages_entities[0][0],
330        CompassOctant::East,
331    );
332
333    // Set initial focus
334    commands.entity(pages_entities[0][0]).insert(AutoFocus);
335}
336
337/// Instruction text for the example
338fn 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
361/// Focus display - shows which button is currently focused
362fn 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
383/// Key display - shows the last key pressed
384fn 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
405/// Creates the buttons for a grid page and returns their ids as a Vec.
406///
407/// The entity ids are needed to specify manual edges, so the scenes are spawned individually.
408fn setup_buttons_for_grid_page(commands: &mut Commands, page_num: usize) -> Vec<Entity> {
409    // Spawn buttons in a grid
410    // Auto-navigation will automatically configure navigation within rows.
411    let button_positions = [
412        // Row 0
413        [(450.0, 80.0), (650.0, 80.0), (850.0, 80.0)],
414        // Row 1
415        [(450.0, 215.0), (650.0, 215.0), (850.0, 215.0)],
416        // Row 2
417        [(450.0, 350.0), (650.0, 350.0), (850.0, 350.0)],
418        // Row 3
419        [(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
427/// Scene list containing all the text entities regarding button/page navigation for a grid page.
428fn 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        // For the first page, add a notice about vertical navigation being blocked off
432        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        // For the third page, add a notice about vertical navigation being inverted in the grid.
436        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        // Text describing current page
444        @helper_text_node_scene(format!("Currently on Page {}", page_num + 1), 650, 10, Justify::Center)
445        --
446        // Text describing direction to go to the previous page, placed left of the top-left button.
447        @helper_text_node_scene(format!("Page {} << ", previous_page), 310, 120, Justify::Right)
448        --
449        // Text describing direction to go to the next page, placed right of the bottom-right button.
450        @helper_text_node_scene(format!(">> Page {}", (page_num + 1) % 3 + 1), 1000, 525, Justify::Left)
451        --
452        // Texts describing that moving right wraps to the next row.
453        @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        // Footer Text
466        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
482/// Creates the buttons for the triangle page and returns their ids as a Vec.
483///
484/// The entity ids are needed to specify manual edges, so the scenes are spawned individually.
485fn setup_buttons_for_triangle_page(commands: &mut Commands, page_num: usize) -> Vec<Entity> {
486    let button_positions = [
487        (450.0, 80.0),   // top left
488        (700.0, 80.0),   // top right
489        (575.0, 215.0),  // middle
490        (1050.0, 350.0), // bottom right
491    ];
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
502/// Scene list containing all the text entities regarding button/page navigation for the triangle page.
503fn 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        // Text describing current page
507        @helper_text_node_scene(format!("Currently on Page {}", page_num + 1), 650, 20, Justify::Center)
508        --
509        // Text describing direction to go to the previous page, placed left of the top-left button.
510        @helper_text_node_scene(format!("Page {} << ", previous_page), 310, 120, Justify::Right)
511        --
512        // Direction to navigate from button 3 to button 4, placed below center button
513        @helper_text_node_scene("v\nBtn 4".into(), 575, 325, Justify::Center)
514        --
515        // Direction to navigate from button 3 to button 4, placed right of center button
516        @helper_text_node_scene("> Btn 4".into(), 735, 255, Justify::Left)
517        --
518        // Direction to navigate from button 4 to button 3, placed above bottom right button
519        @helper_text_node_scene("Btn 3\n^".into(), 1050, 300, Justify::Center)
520        --
521        // Direction to navigate from button 4 to button 3, placed left of bottom right button
522        @helper_text_node_scene("Btn 3 < ".into(), 910, 390, Justify::Right)
523        --
524        // Direction to go to the next page, placed bottom of the bottom-right button.
525        @helper_text_node_scene(format!("V\nV\nPage {}", (page_num + 1) % 3 + 1), 1050, 460, Justify::Center)
526    }
527}
528
529/// The Vec of button scenes for a grid page.
530/// A vec is returned so that they may be iterated upon, as opposed to a `SceneList`.
531fn 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
549/// One auto nav button as a scene.
550fn 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        // This ensures that navigating to this button via a click ensures the
555        // focus is correctly set to this button.
556        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        // Just add this component for automatic navigation
571        AutoDirectionalNavigation
572        ResetTimer
573        Name(text)
574        Children [
575            Text(text_clone)
576            TextLayout {
577                justify: Justify::Center,
578            }
579        ]
580    }
581}
582
583/// One helper text node as a scene
584fn 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// Action state and input handling
606#[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    // Use Dir2::from_xy to convert input to direction, then convert to CompassOctant
695    let maybe_direction = Dir2::from_xy(net_east_west as f32, net_north_south as f32)
696        .ok()
697        .map(CompassOctant::from);
698
699    // Store the previous focus in case navigation switches pages.
700    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                // Successfully navigated!
705
706                // If navigation switches between pages, change the visibilities of pages
707                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                // Navigation failed (no neighbor in that direction)
725            }
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        // Check for keyboard inputs
755        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        // Check for gamepad inputs
770        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}