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