1use core::time::Duration;
18
19use bevy::{
20 camera::NormalizedRenderTarget,
21 input_focus::{
22 directional_navigation::{AutoNavigationConfig, DirectionalNavigationPlugin},
23 FocusCause, 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
35fn main() {
36 App::new()
37 .add_plugins((DefaultPlugins, DirectionalNavigationPlugin))
38 .insert_resource(InputFocusVisible(true))
41 .insert_resource(AutoNavigationConfig {
43 min_alignment_factor: 0.1,
45 max_search_distance: Some(500.0),
47 prefer_aligned: true,
49 })
50 .init_resource::<ActionState>()
51 .add_systems(Startup, setup_scattered_ui)
52 .add_systems(PreUpdate, (process_inputs, navigate).chain())
55 .add_systems(
56 Update,
57 (
58 highlight_focused_element,
59 interact_with_focused_button,
60 reset_button_after_interaction,
61 update_focus_display
62 .run_if(|input_focus: Res<InputFocus>| input_focus.is_changed()),
63 update_key_display,
64 ),
65 )
66 .add_observer(universal_button_click_behavior)
67 .run();
68}
69
70const NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_400;
71const PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_500;
72const FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::BLUE_50;
73
74#[derive(Component)]
76struct FocusDisplay;
77
78#[derive(Component)]
80struct KeyDisplay;
81
82fn universal_button_click_behavior(
84 mut click: On<Pointer<Click>>,
85 mut button_query: Query<(&mut BackgroundColor, &mut ResetTimer)>,
86) {
87 let button_entity = click.entity;
88 if let Ok((mut color, mut reset_timer)) = button_query.get_mut(button_entity) {
89 color.0 = PRESSED_BUTTON.into();
90 reset_timer.0 = Timer::from_seconds(0.3, TimerMode::Once);
91 click.propagate(false);
92 }
93}
94
95#[derive(Component, Default, Deref, DerefMut)]
96struct ResetTimer(Timer);
97
98fn reset_button_after_interaction(
99 time: Res<Time>,
100 mut query: Query<(&mut ResetTimer, &mut BackgroundColor)>,
101) {
102 for (mut reset_timer, mut color) in query.iter_mut() {
103 reset_timer.tick(time.delta());
104 if reset_timer.just_finished() {
105 color.0 = NORMAL_BUTTON.into();
106 }
107 }
108}
109
110fn setup_scattered_ui(mut commands: Commands, mut input_focus: ResMut<InputFocus>) {
115 commands.spawn(Camera2d);
116
117 let root_node = commands
119 .spawn(Node {
120 width: percent(100),
121 height: percent(100),
122 ..default()
123 })
124 .id();
125
126 let instructions = commands
128 .spawn((
129 Text::new(
130 "Directional Navigation Demo\n\n\
131 Use arrow keys or D-pad to navigate.\n\
132 Press Enter or A button to interact.\n\n\
133 Buttons are scattered irregularly,\n\
134 but navigation is automatic!",
135 ),
136 Node {
137 position_type: PositionType::Absolute,
138 left: px(20),
139 top: px(20),
140 width: px(280),
141 padding: UiRect::all(px(12)),
142 border_radius: BorderRadius::all(px(8)),
143 ..default()
144 },
145 BackgroundColor(Color::srgba(0.1, 0.1, 0.1, 0.8)),
146 ))
147 .id();
148
149 commands.spawn((
151 Text::new("Focused: None"),
152 FocusDisplay,
153 Node {
154 position_type: PositionType::Absolute,
155 left: px(20),
156 bottom: px(80),
157 width: px(280),
158 padding: UiRect::all(px(12)),
159 border_radius: BorderRadius::all(px(8)),
160 ..default()
161 },
162 BackgroundColor(Color::srgba(0.1, 0.5, 0.1, 0.8)),
163 TextFont {
164 font_size: FontSize::Px(20.0),
165 ..default()
166 },
167 ));
168
169 commands.spawn((
171 Text::new("Last Key: None"),
172 KeyDisplay,
173 Node {
174 position_type: PositionType::Absolute,
175 left: px(20),
176 bottom: px(20),
177 width: px(280),
178 padding: UiRect::all(px(12)),
179 border_radius: BorderRadius::all(px(8)),
180 ..default()
181 },
182 BackgroundColor(Color::srgba(0.5, 0.1, 0.5, 0.8)),
183 TextFont {
184 font_size: FontSize::Px(20.0),
185 ..default()
186 },
187 ));
188
189 let button_positions = [
192 (350.0, 100.0),
194 (520.0, 120.0),
195 (700.0, 90.0),
196 (380.0, 220.0),
198 (600.0, 240.0),
199 (450.0, 340.0),
201 (620.0, 360.0),
202 (360.0, 480.0),
204 (540.0, 460.0),
205 (720.0, 490.0),
206 ];
207
208 let mut first_button = None;
209 for (i, (x, y)) in button_positions.iter().enumerate() {
210 let transform = if i == 4 {
211 UiTransform {
212 scale: Vec2::splat(1.2),
213 rotation: Rot2::FRAC_PI_2,
214 ..default()
215 }
216 } else {
217 UiTransform::IDENTITY
218 };
219 let button_entity = commands
220 .spawn((
221 Button,
222 Node {
223 position_type: PositionType::Absolute,
224 left: px(*x),
225 top: px(*y),
226 width: px(140),
227 height: px(80),
228 border: UiRect::all(px(4)),
229 justify_content: JustifyContent::Center,
230 align_items: AlignItems::Center,
231 border_radius: BorderRadius::all(px(12)),
232 ..default()
233 },
234 transform,
235 AutoDirectionalNavigation::default(),
237 ResetTimer::default(),
238 BackgroundColor::from(NORMAL_BUTTON),
239 Name::new(format!("Button {}", i + 1)),
240 ))
241 .with_child((
242 Text::new(format!("Button {}", i + 1)),
243 TextLayout {
244 justify: Justify::Center,
245 ..default()
246 },
247 ))
248 .id();
249
250 if first_button.is_none() {
251 first_button = Some(button_entity);
252 }
253 }
254
255 commands.entity(root_node).add_children(&[instructions]);
256
257 if let Some(button) = first_button {
259 input_focus.set(button, FocusCause::Navigated);
260 }
261}
262
263#[derive(Debug, PartialEq, Eq, Hash)]
265enum DirectionalNavigationAction {
266 Up,
267 Down,
268 Left,
269 Right,
270 Select,
271}
272
273impl DirectionalNavigationAction {
274 fn variants() -> Vec<Self> {
275 vec![
276 DirectionalNavigationAction::Up,
277 DirectionalNavigationAction::Down,
278 DirectionalNavigationAction::Left,
279 DirectionalNavigationAction::Right,
280 DirectionalNavigationAction::Select,
281 ]
282 }
283
284 fn keycode(&self) -> KeyCode {
285 match self {
286 DirectionalNavigationAction::Up => KeyCode::ArrowUp,
287 DirectionalNavigationAction::Down => KeyCode::ArrowDown,
288 DirectionalNavigationAction::Left => KeyCode::ArrowLeft,
289 DirectionalNavigationAction::Right => KeyCode::ArrowRight,
290 DirectionalNavigationAction::Select => KeyCode::Enter,
291 }
292 }
293
294 fn gamepad_button(&self) -> GamepadButton {
295 match self {
296 DirectionalNavigationAction::Up => GamepadButton::DPadUp,
297 DirectionalNavigationAction::Down => GamepadButton::DPadDown,
298 DirectionalNavigationAction::Left => GamepadButton::DPadLeft,
299 DirectionalNavigationAction::Right => GamepadButton::DPadRight,
300 DirectionalNavigationAction::Select => GamepadButton::South,
301 }
302 }
303}
304
305#[derive(Default, Resource)]
306struct ActionState {
307 pressed_actions: HashSet<DirectionalNavigationAction>,
308}
309
310fn process_inputs(
311 mut action_state: ResMut<ActionState>,
312 keyboard_input: Res<ButtonInput<KeyCode>>,
313 gamepad_input: Query<&Gamepad>,
314) {
315 action_state.pressed_actions.clear();
316
317 for action in DirectionalNavigationAction::variants() {
318 if keyboard_input.just_pressed(action.keycode()) {
319 action_state.pressed_actions.insert(action);
320 }
321 }
322
323 for gamepad in gamepad_input.iter() {
324 for action in DirectionalNavigationAction::variants() {
325 if gamepad.just_pressed(action.gamepad_button()) {
326 action_state.pressed_actions.insert(action);
327 }
328 }
329 }
330}
331
332fn navigate(
333 action_state: Res<ActionState>,
334 mut auto_directional_navigator: AutoDirectionalNavigator,
335) {
336 let net_east_west = action_state
337 .pressed_actions
338 .contains(&DirectionalNavigationAction::Right) as i8
339 - action_state
340 .pressed_actions
341 .contains(&DirectionalNavigationAction::Left) as i8;
342
343 let net_north_south = action_state
344 .pressed_actions
345 .contains(&DirectionalNavigationAction::Up) as i8
346 - action_state
347 .pressed_actions
348 .contains(&DirectionalNavigationAction::Down) as i8;
349
350 let maybe_direction = Dir2::from_xy(net_east_west as f32, net_north_south as f32)
352 .ok()
353 .map(CompassOctant::from);
354
355 if let Some(direction) = maybe_direction {
356 match auto_directional_navigator.navigate(direction) {
357 Ok(_entity) => {
358 }
360 Err(_e) => {
361 }
363 }
364 }
365}
366
367fn update_focus_display(
368 input_focus: Res<InputFocus>,
369 button_query: Query<&Name, With<Button>>,
370 mut display_query: Query<&mut Text, With<FocusDisplay>>,
371) {
372 if let Ok(mut text) = display_query.single_mut() {
373 if let Some(focused_entity) = input_focus.get() {
374 if let Ok(name) = button_query.get(focused_entity) {
375 **text = format!("Focused: {}", name);
376 } else {
377 **text = "Focused: Unknown".to_string();
378 }
379 } else {
380 **text = "Focused: None".to_string();
381 }
382 }
383}
384
385fn update_key_display(
386 keyboard_input: Res<ButtonInput<KeyCode>>,
387 gamepad_input: Query<&Gamepad>,
388 mut display_query: Query<&mut Text, With<KeyDisplay>>,
389) {
390 if let Ok(mut text) = display_query.single_mut() {
391 for action in DirectionalNavigationAction::variants() {
393 if keyboard_input.just_pressed(action.keycode()) {
394 let key_name = match action {
395 DirectionalNavigationAction::Up => "Up Arrow",
396 DirectionalNavigationAction::Down => "Down Arrow",
397 DirectionalNavigationAction::Left => "Left Arrow",
398 DirectionalNavigationAction::Right => "Right Arrow",
399 DirectionalNavigationAction::Select => "Enter",
400 };
401 **text = format!("Last Key: {}", key_name);
402 return;
403 }
404 }
405
406 for gamepad in gamepad_input.iter() {
408 for action in DirectionalNavigationAction::variants() {
409 if gamepad.just_pressed(action.gamepad_button()) {
410 let button_name = match action {
411 DirectionalNavigationAction::Up => "D-Pad Up",
412 DirectionalNavigationAction::Down => "D-Pad Down",
413 DirectionalNavigationAction::Left => "D-Pad Left",
414 DirectionalNavigationAction::Right => "D-Pad Right",
415 DirectionalNavigationAction::Select => "A Button",
416 };
417 **text = format!("Last Key: {}", button_name);
418 return;
419 }
420 }
421 }
422 }
423}
424
425fn highlight_focused_element(
426 input_focus: Res<InputFocus>,
427 input_focus_visible: Res<InputFocusVisible>,
428 mut query: Query<(Entity, &mut BorderColor)>,
429) {
430 for (entity, mut border_color) in query.iter_mut() {
431 if input_focus.get() == Some(entity) && input_focus_visible.0 {
432 *border_color = BorderColor::all(FOCUSED_BORDER);
433 } else {
434 *border_color = BorderColor::DEFAULT;
435 }
436 }
437}
438
439fn interact_with_focused_button(
440 action_state: Res<ActionState>,
441 input_focus: Res<InputFocus>,
442 mut commands: Commands,
443) {
444 if action_state
445 .pressed_actions
446 .contains(&DirectionalNavigationAction::Select)
447 && let Some(focused_entity) = input_focus.get()
448 {
449 commands.trigger(Pointer::new(
450 PointerId::Mouse,
451 Location {
452 target: NormalizedRenderTarget::None {
453 width: 0,
454 height: 0,
455 },
456 position: Vec2::ZERO,
457 },
458 Click {
459 button: PointerButton::Primary,
460 hit: HitData {
461 camera: Entity::PLACEHOLDER,
462 depth: 0.0,
463 position: None,
464 normal: None,
465 extra: None,
466 },
467 count: 1,
468 duration: Duration::from_secs_f32(0.1),
469 },
470 focused_entity,
471 ));
472 }
473}