docs/bevy/examples/ui/navigation/directional_navigation_overrides.rs 31 K · 836 lines · raw · history

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
20 use core::time::Duration;
21
22 use 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
42 fn 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
82 const PAGE_1_NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_400;
83 const PAGE_1_PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_500;
84 const PAGE_1_FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::BLUE_50;
85
86 const PAGE_2_NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::RED_400;
87 const PAGE_2_PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::RED_500;
88 const PAGE_2_FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::RED_50;
89
90 const PAGE_3_NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::GREEN_400;
91 const PAGE_3_PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::GREEN_500;
92 const PAGE_3_FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::GREEN_50;
93
94 const NORMAL_BUTTON_COLORS: [Srgba; 3] = [
95 PAGE_1_NORMAL_BUTTON,
96 PAGE_2_NORMAL_BUTTON,
97 PAGE_3_NORMAL_BUTTON,
98 ];
99 const PRESSED_BUTTON_COLORS: [Srgba; 3] = [
100 PAGE_1_PRESSED_BUTTON,
101 PAGE_2_PRESSED_BUTTON,
102 PAGE_3_PRESSED_BUTTON,
103 ];
104 const 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)]
112 struct FocusDisplay;
113
114 /// Marker component for the text that displays the last key pressed
115 #[derive(Component, Clone, Copy, Default)]
116 struct KeyDisplay;
117
118 /// Component that stores which page a button is on
119 #[derive(Component, Clone, Copy, Default)]
120 struct 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
125 fn 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)]
142 struct ResetTimer(Timer);
143
144 fn 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`].
168 fn 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
338 fn 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
362 fn 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
384 fn 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.
408 fn 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.
428 fn 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.
485 fn 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.
503 fn 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`.
531 fn 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.
550 fn 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
584 fn 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)]
607 enum DirectionalNavigationAction {
608 Up,
609 Down,
610 Left,
611 Right,
612 Select,
613 }
614
615 impl 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)]
648 struct ActionState {
649 pressed_actions: HashSet<DirectionalNavigationAction>,
650 }
651
652 fn 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
674 fn 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
730 fn 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
748 fn 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
788 fn 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
802 fn 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 }