diff --git a/Cargo.toml b/Cargo.toml index b8fb95fb2da4b..d649d14573787 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4014,6 +4014,17 @@ doc-scrape-examples = true [package.metadata.example.tab_navigation] name = "Tab Navigation" -description = "Demonstration of Tab Navigation" +description = "Demonstration of Tab Navigation between UI elements" +category = "UI (User Interface)" +wasm = true + +[[example]] +name = "directional_navigation" +path = "examples/ui/directional_navigation.rs" +doc-scrape-examples = true + +[package.metadata.example.directional_navigation] +name = "Directional Navigation" +description = "Demonstration of Directional Navigation between UI elements" category = "UI (User Interface)" wasm = true diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 23da25d3ce1e4..2e63ad339c830 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -114,7 +114,9 @@ impl InputFocus { /// By contrast, a console-style UI intended to be navigated with a gamepad may always have the focus indicator visible. /// /// To easily access information about whether focus indicators should be shown for a given entity, use the [`IsFocused`] trait. -#[derive(Clone, Debug, Resource)] +/// +/// By default, this resource is set to `false`. +#[derive(Clone, Debug, Resource, Default)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Resource))] pub struct InputFocusVisible(pub bool); @@ -174,8 +176,8 @@ pub struct InputDispatchPlugin; impl Plugin for InputDispatchPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, set_initial_focus) - .insert_resource(InputFocus(None)) - .insert_resource(InputFocusVisible(false)) + .init_resource::() + .init_resource::() .add_systems( PreUpdate, ( diff --git a/crates/bevy_winit/src/accessibility.rs b/crates/bevy_winit/src/accessibility.rs index 078f47ae4377a..8d1c5db0c4ceb 100644 --- a/crates/bevy_winit/src/accessibility.rs +++ b/crates/bevy_winit/src/accessibility.rs @@ -198,6 +198,14 @@ fn update_accessibility_nodes( return; }; if focus.is_changed() || !nodes.is_empty() { + // Don't panic if the focused entity does not currently exist + // It's probably waiting to be spawned + if let Some(focused_entity) = focus.0 { + if !node_entities.contains(focused_entity) { + return; + } + } + adapter.update_if_active(|| { update_adapter( nodes, diff --git a/examples/README.md b/examples/README.md index 49a90a422423f..e85156ca01bf3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -512,6 +512,7 @@ Example | Description [Box Shadow](../examples/ui/box_shadow.rs) | Demonstrates how to create a node with a shadow [Button](../examples/ui/button.rs) | Illustrates creating and updating a button [CSS Grid](../examples/ui/grid.rs) | An example for CSS Grid layout +[Directional Navigation](../examples/ui/directional_navigation.rs) | Demonstration of Directional Navigation between UI elements [Display and Visibility](../examples/ui/display_and_visibility.rs) | Demonstrates how Display and Visibility work in the UI. [Flex Layout](../examples/ui/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text [Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally) @@ -523,7 +524,7 @@ Example | Description [Render UI to Texture](../examples/ui/render_ui_to_texture.rs) | An example of rendering UI as a part of a 3D world [Scroll](../examples/ui/scroll.rs) | Demonstrates scrolling UI containers [Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node. -[Tab Navigation](../examples/ui/tab_navigation.rs) | Demonstration of Tab Navigation +[Tab Navigation](../examples/ui/tab_navigation.rs) | Demonstration of Tab Navigation between UI elements [Text](../examples/ui/text.rs) | Illustrates creating and updating text [Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout [Text Wrap Debug](../examples/ui/text_wrap_debug.rs) | Demonstrates text wrapping diff --git a/examples/ui/directional_navigation.rs b/examples/ui/directional_navigation.rs new file mode 100644 index 0000000000000..a5b4286405999 --- /dev/null +++ b/examples/ui/directional_navigation.rs @@ -0,0 +1,417 @@ +//! Demonstrates how to set up the directional navigation system to allow for navigation between widgets. +//! +//! Directional navigation is generally used to move between widgets in a user interface using arrow keys or gamepad input. +//! When compared to tab navigation, directional navigation is generally more direct, and less aware of the structure of the UI. +//! +//! In this example, we will set up a simple UI with a grid of buttons that can be navigated using the arrow keys or gamepad input. + +use bevy::{ + input_focus::{ + directional_navigation::{ + DirectionalNavigation, DirectionalNavigationMap, DirectionalNavigationPlugin, + }, + InputDispatchPlugin, InputFocus, InputFocusVisible, + }, + math::{CompassOctant, FloatOrd}, + picking::{ + backend::HitData, + pointer::{Location, PointerId}, + }, + prelude::*, + render::camera::NormalizedRenderTarget, + utils::{HashMap, HashSet}, +}; + +fn main() { + App::new() + // Input focus is not enabled by default, so we need to add the corresponding plugins + .add_plugins(( + DefaultPlugins, + InputDispatchPlugin, + DirectionalNavigationPlugin, + )) + // This resource is canonically used to track whether or not to render a focus indicator + // It starts as false, but we set it to true here as we would like to see the focus indicator + .insert_resource(InputFocusVisible(true)) + // We've made a simple resource to keep track of the actions that are currently being pressed for this example + .init_resource::() + .add_systems(Startup, setup_ui) + // Input is generally handled during PreUpdate + // We're turning inputs into actions first, then using those actions to determine navigation + .add_systems(PreUpdate, (process_inputs, navigate).chain()) + .add_systems( + Update, + ( + // We need to show which button is currently focused + highlight_focused_element, + // Pressing the "Interact" button while we have a focused element should simulate a click + interact_with_focused_button, + // We're doing a tiny animation when the button is interacted with, + // so we need a timer and a polling mechanism to reset it + reset_button_after_interaction, + ), + ) + // This observer is added globally, so it will respond to *any* trigger of the correct type. + // However, we're filtering in the observer's query to only respond to button presses + .add_observer(universal_button_click_behavior) + .run(); +} + +const NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_400; +const PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_500; +const FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::BLUE_50; + +// This observer will be triggered whenever a button is pressed +// In a real project, each button would also have its own unique behavior, +// to capture the actual intent of the user +fn universal_button_click_behavior( + // We're using a on-mouse-down trigger to improve responsiveness; + // Clicked is better when you want roll-off cancellation + mut trigger: Trigger>, + mut button_query: Query<(&mut BackgroundColor, &mut ResetTimer)>, +) { + let button_entity = trigger.target(); + if let Ok((mut color, mut reset_timer)) = button_query.get_mut(button_entity) { + // This would be a great place to play a little sound effect too! + color.0 = PRESSED_BUTTON.into(); + reset_timer.0 = Timer::from_seconds(0.3, TimerMode::Once); + + // Picking events propagate up the hierarchy, + // so we need to stop the propagation here now that we've handled it + trigger.propagate(false); + } +} + +/// Resets a UI element to its default state when the timer has elapsed. +#[derive(Component, Default, Deref, DerefMut)] +struct ResetTimer(Timer); + +fn reset_button_after_interaction( + time: Res