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