Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/ui/navigation/directional_navigation.rs
9354 views
1
//! Demonstrates automatic directional navigation.
2
//!
3
//! This shows how to use automatic navigation by simply adding the [`AutoDirectionalNavigation`]
4
//! component to UI elements. Navigation is automatically calculated based on screen positions.
5
//!
6
//! This is especially useful for:
7
//! - Dynamic UIs where elements may be added, removed, or repositioned
8
//! - Irregular layouts that don't fit a simple grid pattern
9
//! - Prototyping where you want navigation without tedious manual setup
10
//!
11
//! The automatic system finds the nearest neighbor in each compass direction for every node,
12
//! completely eliminating the need to manually specify navigation relationships.
13
//!
14
//! For an example that demonstrates automatic directional navigation with manual overrides,
15
//! refer to the `directional_navigation_overrides` example.
16
17
use core::time::Duration;
18
19
use bevy::{
20
camera::NormalizedRenderTarget,
21
input_focus::{
22
directional_navigation::{AutoNavigationConfig, DirectionalNavigationPlugin},
23
InputDispatchPlugin, InputFocus, InputFocusVisible,
24
},
25
math::{CompassOctant, Dir2, Rot2},
26
picking::{
27
backend::HitData,
28
pointer::{Location, PointerId},
29
},
30
platform::collections::HashSet,
31
prelude::*,
32
ui::auto_directional_navigation::{AutoDirectionalNavigation, AutoDirectionalNavigator},
33
};
34
35
fn main() {
36
App::new()
37
// Input focus is not enabled by default, so we need to add the corresponding plugins
38
.add_plugins((
39
DefaultPlugins,
40
InputDispatchPlugin,
41
DirectionalNavigationPlugin,
42
))
43
// This resource is canonically used to track whether or not to render a focus indicator
44
// It starts as false, but we set it to true here as we would like to see the focus indicator
45
.insert_resource(InputFocusVisible(true))
46
// Configure auto-navigation behavior
47
.insert_resource(AutoNavigationConfig {
48
// Require at least 10% overlap in perpendicular axis for cardinal directions
49
min_alignment_factor: 0.1,
50
// Don't connect nodes more than 500 pixels apart between their closest edges
51
max_search_distance: Some(500.0),
52
// Prefer nodes that are well-aligned
53
prefer_aligned: true,
54
})
55
.init_resource::<ActionState>()
56
.add_systems(Startup, setup_scattered_ui)
57
// No manual system needed - just add AutoDirectionalNavigation to entities.
58
// Input is generally handled during PreUpdate
59
.add_systems(PreUpdate, (process_inputs, navigate).chain())
60
.add_systems(
61
Update,
62
(
63
highlight_focused_element,
64
interact_with_focused_button,
65
reset_button_after_interaction,
66
update_focus_display
67
.run_if(|input_focus: Res<InputFocus>| input_focus.is_changed()),
68
update_key_display,
69
),
70
)
71
.add_observer(universal_button_click_behavior)
72
.run();
73
}
74
75
const NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_400;
76
const PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_500;
77
const FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::BLUE_50;
78
79
/// Marker component for the text that displays the currently focused button
80
#[derive(Component)]
81
struct FocusDisplay;
82
83
/// Marker component for the text that displays the last key pressed
84
#[derive(Component)]
85
struct KeyDisplay;
86
87
// Observer for button clicks
88
fn universal_button_click_behavior(
89
mut click: On<Pointer<Click>>,
90
mut button_query: Query<(&mut BackgroundColor, &mut ResetTimer)>,
91
) {
92
let button_entity = click.entity;
93
if let Ok((mut color, mut reset_timer)) = button_query.get_mut(button_entity) {
94
color.0 = PRESSED_BUTTON.into();
95
reset_timer.0 = Timer::from_seconds(0.3, TimerMode::Once);
96
click.propagate(false);
97
}
98
}
99
100
#[derive(Component, Default, Deref, DerefMut)]
101
struct ResetTimer(Timer);
102
103
fn reset_button_after_interaction(
104
time: Res<Time>,
105
mut query: Query<(&mut ResetTimer, &mut BackgroundColor)>,
106
) {
107
for (mut reset_timer, mut color) in query.iter_mut() {
108
reset_timer.tick(time.delta());
109
if reset_timer.just_finished() {
110
color.0 = NORMAL_BUTTON.into();
111
}
112
}
113
}
114
115
/// Spawn a scattered layout of buttons to demonstrate automatic navigation.
116
///
117
/// Unlike a regular grid, these buttons are irregularly positioned,
118
/// but auto-navigation will still figure out the correct connections!
119
fn setup_scattered_ui(mut commands: Commands, mut input_focus: ResMut<InputFocus>) {
120
commands.spawn(Camera2d);
121
122
// Create a full-screen background node
123
let root_node = commands
124
.spawn(Node {
125
width: percent(100),
126
height: percent(100),
127
..default()
128
})
129
.id();
130
131
// Instructions
132
let instructions = commands
133
.spawn((
134
Text::new(
135
"Directional Navigation Demo\n\n\
136
Use arrow keys or D-pad to navigate.\n\
137
Press Enter or A button to interact.\n\n\
138
Buttons are scattered irregularly,\n\
139
but navigation is automatic!",
140
),
141
Node {
142
position_type: PositionType::Absolute,
143
left: px(20),
144
top: px(20),
145
width: px(280),
146
padding: UiRect::all(px(12)),
147
border_radius: BorderRadius::all(px(8)),
148
..default()
149
},
150
BackgroundColor(Color::srgba(0.1, 0.1, 0.1, 0.8)),
151
))
152
.id();
153
154
// Focus display - shows which button is currently focused
155
commands.spawn((
156
Text::new("Focused: None"),
157
FocusDisplay,
158
Node {
159
position_type: PositionType::Absolute,
160
left: px(20),
161
bottom: px(80),
162
width: px(280),
163
padding: UiRect::all(px(12)),
164
border_radius: BorderRadius::all(px(8)),
165
..default()
166
},
167
BackgroundColor(Color::srgba(0.1, 0.5, 0.1, 0.8)),
168
TextFont {
169
font_size: FontSize::Px(20.0),
170
..default()
171
},
172
));
173
174
// Key display - shows the last key pressed
175
commands.spawn((
176
Text::new("Last Key: None"),
177
KeyDisplay,
178
Node {
179
position_type: PositionType::Absolute,
180
left: px(20),
181
bottom: px(20),
182
width: px(280),
183
padding: UiRect::all(px(12)),
184
border_radius: BorderRadius::all(px(8)),
185
..default()
186
},
187
BackgroundColor(Color::srgba(0.5, 0.1, 0.5, 0.8)),
188
TextFont {
189
font_size: FontSize::Px(20.0),
190
..default()
191
},
192
));
193
194
// Spawn buttons in a scattered/irregular pattern
195
// The auto-navigation system will figure out the connections!
196
let button_positions = [
197
// Top row (irregular spacing)
198
(350.0, 100.0),
199
(520.0, 120.0),
200
(700.0, 90.0),
201
// Middle-top row
202
(380.0, 220.0),
203
(600.0, 240.0),
204
// Center
205
(450.0, 340.0),
206
(620.0, 360.0),
207
// Lower row
208
(360.0, 480.0),
209
(540.0, 460.0),
210
(720.0, 490.0),
211
];
212
213
let mut first_button = None;
214
for (i, (x, y)) in button_positions.iter().enumerate() {
215
let transform = if i == 4 {
216
UiTransform {
217
scale: Vec2::splat(1.2),
218
rotation: Rot2::FRAC_PI_2,
219
..default()
220
}
221
} else {
222
UiTransform::IDENTITY
223
};
224
let button_entity = commands
225
.spawn((
226
Button,
227
Node {
228
position_type: PositionType::Absolute,
229
left: px(*x),
230
top: px(*y),
231
width: px(140),
232
height: px(80),
233
border: UiRect::all(px(4)),
234
justify_content: JustifyContent::Center,
235
align_items: AlignItems::Center,
236
border_radius: BorderRadius::all(px(12)),
237
..default()
238
},
239
transform,
240
// This is the key: just add this component for automatic navigation!
241
AutoDirectionalNavigation::default(),
242
ResetTimer::default(),
243
BackgroundColor::from(NORMAL_BUTTON),
244
Name::new(format!("Button {}", i + 1)),
245
))
246
.with_child((
247
Text::new(format!("Button {}", i + 1)),
248
TextLayout {
249
justify: Justify::Center,
250
..default()
251
},
252
))
253
.id();
254
255
if first_button.is_none() {
256
first_button = Some(button_entity);
257
}
258
}
259
260
commands.entity(root_node).add_children(&[instructions]);
261
262
// Set initial focus
263
if let Some(button) = first_button {
264
input_focus.set(button);
265
}
266
}
267
268
// Action state and input handling
269
#[derive(Debug, PartialEq, Eq, Hash)]
270
enum DirectionalNavigationAction {
271
Up,
272
Down,
273
Left,
274
Right,
275
Select,
276
}
277
278
impl DirectionalNavigationAction {
279
fn variants() -> Vec<Self> {
280
vec![
281
DirectionalNavigationAction::Up,
282
DirectionalNavigationAction::Down,
283
DirectionalNavigationAction::Left,
284
DirectionalNavigationAction::Right,
285
DirectionalNavigationAction::Select,
286
]
287
}
288
289
fn keycode(&self) -> KeyCode {
290
match self {
291
DirectionalNavigationAction::Up => KeyCode::ArrowUp,
292
DirectionalNavigationAction::Down => KeyCode::ArrowDown,
293
DirectionalNavigationAction::Left => KeyCode::ArrowLeft,
294
DirectionalNavigationAction::Right => KeyCode::ArrowRight,
295
DirectionalNavigationAction::Select => KeyCode::Enter,
296
}
297
}
298
299
fn gamepad_button(&self) -> GamepadButton {
300
match self {
301
DirectionalNavigationAction::Up => GamepadButton::DPadUp,
302
DirectionalNavigationAction::Down => GamepadButton::DPadDown,
303
DirectionalNavigationAction::Left => GamepadButton::DPadLeft,
304
DirectionalNavigationAction::Right => GamepadButton::DPadRight,
305
DirectionalNavigationAction::Select => GamepadButton::South,
306
}
307
}
308
}
309
310
#[derive(Default, Resource)]
311
struct ActionState {
312
pressed_actions: HashSet<DirectionalNavigationAction>,
313
}
314
315
fn process_inputs(
316
mut action_state: ResMut<ActionState>,
317
keyboard_input: Res<ButtonInput<KeyCode>>,
318
gamepad_input: Query<&Gamepad>,
319
) {
320
action_state.pressed_actions.clear();
321
322
for action in DirectionalNavigationAction::variants() {
323
if keyboard_input.just_pressed(action.keycode()) {
324
action_state.pressed_actions.insert(action);
325
}
326
}
327
328
for gamepad in gamepad_input.iter() {
329
for action in DirectionalNavigationAction::variants() {
330
if gamepad.just_pressed(action.gamepad_button()) {
331
action_state.pressed_actions.insert(action);
332
}
333
}
334
}
335
}
336
337
fn navigate(
338
action_state: Res<ActionState>,
339
mut auto_directional_navigator: AutoDirectionalNavigator,
340
) {
341
let net_east_west = action_state
342
.pressed_actions
343
.contains(&DirectionalNavigationAction::Right) as i8
344
- action_state
345
.pressed_actions
346
.contains(&DirectionalNavigationAction::Left) as i8;
347
348
let net_north_south = action_state
349
.pressed_actions
350
.contains(&DirectionalNavigationAction::Up) as i8
351
- action_state
352
.pressed_actions
353
.contains(&DirectionalNavigationAction::Down) as i8;
354
355
// Use Dir2::from_xy to convert input to direction, then convert to CompassOctant
356
let maybe_direction = Dir2::from_xy(net_east_west as f32, net_north_south as f32)
357
.ok()
358
.map(CompassOctant::from);
359
360
if let Some(direction) = maybe_direction {
361
match auto_directional_navigator.navigate(direction) {
362
Ok(_entity) => {
363
// Successfully navigated
364
}
365
Err(_e) => {
366
// Navigation failed (no neighbor in that direction)
367
}
368
}
369
}
370
}
371
372
fn update_focus_display(
373
input_focus: Res<InputFocus>,
374
button_query: Query<&Name, With<Button>>,
375
mut display_query: Query<&mut Text, With<FocusDisplay>>,
376
) {
377
if let Ok(mut text) = display_query.single_mut() {
378
if let Some(focused_entity) = input_focus.0 {
379
if let Ok(name) = button_query.get(focused_entity) {
380
**text = format!("Focused: {}", name);
381
} else {
382
**text = "Focused: Unknown".to_string();
383
}
384
} else {
385
**text = "Focused: None".to_string();
386
}
387
}
388
}
389
390
fn update_key_display(
391
keyboard_input: Res<ButtonInput<KeyCode>>,
392
gamepad_input: Query<&Gamepad>,
393
mut display_query: Query<&mut Text, With<KeyDisplay>>,
394
) {
395
if let Ok(mut text) = display_query.single_mut() {
396
// Check for keyboard inputs
397
for action in DirectionalNavigationAction::variants() {
398
if keyboard_input.just_pressed(action.keycode()) {
399
let key_name = match action {
400
DirectionalNavigationAction::Up => "Up Arrow",
401
DirectionalNavigationAction::Down => "Down Arrow",
402
DirectionalNavigationAction::Left => "Left Arrow",
403
DirectionalNavigationAction::Right => "Right Arrow",
404
DirectionalNavigationAction::Select => "Enter",
405
};
406
**text = format!("Last Key: {}", key_name);
407
return;
408
}
409
}
410
411
// Check for gamepad inputs
412
for gamepad in gamepad_input.iter() {
413
for action in DirectionalNavigationAction::variants() {
414
if gamepad.just_pressed(action.gamepad_button()) {
415
let button_name = match action {
416
DirectionalNavigationAction::Up => "D-Pad Up",
417
DirectionalNavigationAction::Down => "D-Pad Down",
418
DirectionalNavigationAction::Left => "D-Pad Left",
419
DirectionalNavigationAction::Right => "D-Pad Right",
420
DirectionalNavigationAction::Select => "A Button",
421
};
422
**text = format!("Last Key: {}", button_name);
423
return;
424
}
425
}
426
}
427
}
428
}
429
430
fn highlight_focused_element(
431
input_focus: Res<InputFocus>,
432
input_focus_visible: Res<InputFocusVisible>,
433
mut query: Query<(Entity, &mut BorderColor)>,
434
) {
435
for (entity, mut border_color) in query.iter_mut() {
436
if input_focus.0 == Some(entity) && input_focus_visible.0 {
437
*border_color = BorderColor::all(FOCUSED_BORDER);
438
} else {
439
*border_color = BorderColor::DEFAULT;
440
}
441
}
442
}
443
444
fn interact_with_focused_button(
445
action_state: Res<ActionState>,
446
input_focus: Res<InputFocus>,
447
mut commands: Commands,
448
) {
449
if action_state
450
.pressed_actions
451
.contains(&DirectionalNavigationAction::Select)
452
&& let Some(focused_entity) = input_focus.0
453
{
454
commands.trigger(Pointer::<Click> {
455
entity: focused_entity,
456
pointer_id: PointerId::Mouse,
457
pointer_location: Location {
458
target: NormalizedRenderTarget::None {
459
width: 0,
460
height: 0,
461
},
462
position: Vec2::ZERO,
463
},
464
event: Click {
465
button: PointerButton::Primary,
466
hit: HitData {
467
camera: Entity::PLACEHOLDER,
468
depth: 0.0,
469
position: None,
470
normal: None,
471
},
472
duration: Duration::from_secs_f32(0.1),
473
},
474
});
475
}
476
}
477
478