diff --git a/Cargo.toml b/Cargo.toml index dd904e1217590..c369b3eccd383 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5081,25 +5081,25 @@ wasm = true [[example]] name = "directional_navigation" -path = "examples/ui/directional_navigation.rs" +path = "examples/ui/navigation/directional_navigation.rs" # Causes an ICE on docs.rs doc-scrape-examples = false [package.metadata.example.directional_navigation] name = "Directional Navigation" -description = "Demonstration of Directional Navigation between UI elements" +description = "Demonstration of automatic directional navigation based on UI element positions" category = "UI (User Interface)" wasm = true [[example]] -name = "auto_directional_navigation" -path = "examples/ui/auto_directional_navigation.rs" +name = "directional_navigation_overrides" +path = "examples/ui/navigation/directional_navigation_overrides.rs" # Causes an ICE on docs.rs doc-scrape-examples = false -[package.metadata.example.auto_directional_navigation] -name = "Automatic Directional Navigation" -description = "Demonstration of automatic directional navigation graph generation based on UI element positions" +[package.metadata.example.directional_navigation_overrides] +name = "Directional Navigation Overrides" +description = "Demonstration of automatic directional navigation between UI elements with manual overrides" category = "UI (User Interface)" wasm = true diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index a7b45788d5fe0..9a9da75aff853 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -1,4 +1,9 @@ -//! A navigation framework for moving between focusable elements based on directional input. +//! A manual navigation framework for moving between focusable elements based on directional input. +//! +//! Note: If using `bevy_ui`, this manual navigation framework is used to provide overrides +//! for its automatic navigation framework based on the `AutoDirectionalNavigation` component. +//! Most times, the automatic navigation framework alone should be sufficient. +//! If not using `bevy_ui`, this manual navigation framework can still be used by itself. //! //! While virtual cursors are a common way to navigate UIs with a gamepad (or arrow keys!), //! they are generally both slow and frustrating to use. @@ -7,15 +12,15 @@ //! Like the rest of this crate, the [`InputFocus`] resource is manipulated to track //! the current focus. //! +//! This module's [`DirectionalNavigationMap`] stores a directed graph of focusable entities. +//! Each entity can have up to 8 neighbors, one for each [`CompassOctant`], balancing +//! flexibility and required precision. +//! //! Navigating between focusable entities (commonly UI nodes) is done by //! passing a [`CompassOctant`] into the [`navigate`](DirectionalNavigation::navigate) method -//! from the [`DirectionalNavigation`] system parameter. Under the hood, an entity is found -//! automatically via brute force search in the desired [`CompassOctant`] direction. -//! -//! If some manual navigation is desired, a [`DirectionalNavigationMap`] will override the brute force -//! search in a direction for a given entity. The [`DirectionalNavigationMap`] stores a directed graph -//! of focusable entities. Each entity can have up to 8 neighbors, one for each [`CompassOctant`], -//! balancing flexibility and required precision. +//! from the [`DirectionalNavigation`] system parameter. Under the hood, the +//! [`DirectionalNavigationMap`] is used to return the focusable entity in a direction +//! for a given entity. //! //! # Setting up Directional Navigation //! @@ -26,21 +31,26 @@ //! include automatic navigation, you should also use the `AutoDirectionalNavigator` system parameter //! in that crate instead of [`DirectionalNavigation`]. //! -//! ## Manual Navigation -//! -//! You can also manually define navigation connections using methods like -//! [`add_edge`](DirectionalNavigationMap::add_edge) and -//! [`add_looping_edges`](DirectionalNavigationMap::add_looping_edges). -//! -//! ## Combining Automatic and Manual +//! ## Combining Automatic Navigation with Manual Overrides //! //! Following manual edges always take precedence, allowing you to use //! automatic navigation for most UI elements while overriding specific connections for -//! special cases like wrapping menus or cross-layer navigation. +//! special cases like wrapping menus or cross-layer navigation. If you need to override +//! automatic navigation behavior, use the [`DirectionalNavigationMap`] to define +//! overriding edges between UI entities. +//! +//! ## Manual Navigation Only +//! +//! Manually define your navigation using the [`DirectionalNavigationMap`], and use the +//! [`DirectionalNavigation`] system parameter to navigate between components. +//! You can define navigation connections using methods like +//! [`add_edge`](DirectionalNavigationMap::add_edge) and +//! [`add_looping_edges`](DirectionalNavigationMap::add_looping_edges). //! -//! ## When to Use Manual Navigation +//! ## When to Use Manual Navigation or Manual Overrides //! -//! While automatic navigation is recommended for most use cases, manual navigation provides: +//! While automatic navigation is recommended and satisfactory for most use cases, +//! using manual navigation only or integrating manual overrides to automatic navigation provide: //! //! - **Precise control**: Define exact navigation flow, including non-obvious connections like looping edges //! - **Cross-layer navigation**: Connect elements across different UI layers or z-index levels @@ -122,6 +132,7 @@ pub struct AutoNavigationConfig { /// Maximum search distance in logical pixels. /// /// Nodes beyond this distance won't be connected. `None` means unlimited. + /// The distance between two UI elements is calculated using their closest edges. pub max_search_distance: Option, /// Whether to prefer nodes that are more aligned with the exact direction. @@ -189,6 +200,11 @@ impl NavNeighbors { /// /// This graph must be built and maintained manually, and the developer is responsible for ensuring that it meets the above criteria. /// Notably, if the developer adds or removes the navigability of an entity, the developer should update the map as necessary. +/// +/// If the automatic navigation system in `bevy_ui` is being used, this resource can be used to specify +/// manual navigation overrides. Any navigation edges specified in this map take precedence over automatic +/// navigation. For example, if navigation on one side of the window should wrap around to +/// the other side of the window, this navigation behavior can be specified using this map. #[derive(Resource, Debug, Default, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", diff --git a/crates/bevy_ui/src/auto_directional_navigation.rs b/crates/bevy_ui/src/auto_directional_navigation.rs index a3072e486f443..eb650d0d25896 100644 --- a/crates/bevy_ui/src/auto_directional_navigation.rs +++ b/crates/bevy_ui/src/auto_directional_navigation.rs @@ -1,7 +1,21 @@ -//! An automatic directional navigation system, powered by the [`AutoDirectionalNavigation`] component. +//! An automatic directional navigation system, powered by the [`AutoDirectionalNavigation`] +//! component. //! -//! [`AutoDirectionalNavigator`] expands on the manual directional navigation system -//! provided by the [`DirectionalNavigation`] system parameter from `bevy_input_focus`. +//! Unlike the navigation system provided by `bevy_input_focus`, the automatic directional +//! navigation system does not require specifying navigation edges. Just simply add the +//! [`AutoDirectionalNavigation`] component to your entities, and the system will automatically +//! calculate the navigation edges between entities based on screen position. +//! +//! [`AutoDirectionalNavigator`] replaces the manual directional navigation system +//! provided by the [`DirectionalNavigation`] system parameter from `bevy_input_focus`. The +//! [`AutoDirectionalNavigator`] will first navigate using manual override edges defined in the +//! [`DirectionalNavigationMap`](bevy_input_focus::directional_navigation::DirectionalNavigationMap). +//! If no manual overrides are defined, automatic navigation will occur between entities based on +//! screen position. +//! +//! If any resulting navigation behavior is undesired, [`AutoNavigationConfig`] can be tweaked or +//! manual overrides can be specified using the +//! [`DirectionalNavigationMap`](bevy_input_focus::directional_navigation::DirectionalNavigationMap). use crate::{ComputedNode, ComputedUiTargetCamera, UiGlobalTransform}; use bevy_camera::visibility::InheritedVisibility; @@ -72,7 +86,7 @@ use bevy_reflect::{prelude::*, Reflect}; /// automatic navigation as needed. /// /// Manual edges defined via [`DirectionalNavigationMap`](bevy_input_focus::directional_navigation::DirectionalNavigationMap) -/// are completely independent and will continue to work regardless of this component. +/// will override any automatically calculated edges. /// /// # Additional Requirements /// @@ -237,6 +251,11 @@ impl<'w, 's> AutoDirectionalNavigator<'w, 's> { } } +/// Util used to get the resulting bounds of a UI entity after applying its rotation. +/// +/// This is necessary to apply because navigation should only use the final screen position +/// of an entity in automatic navigation calculations. These bounds are used as the entity's size in +/// [`FocusableArea`]. fn get_rotated_bounds(size: Vec2, rotation: f32) -> Vec2 { if rotation == 0.0 { return size; diff --git a/examples/README.md b/examples/README.md index 487832ecb3543..5c33a02e2bc8b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -567,12 +567,12 @@ Example | Description Example | Description --- | --- [Anchor Layout](../examples/ui/anchor_layout.rs) | Shows an 'anchor layout' style of ui layout -[Automatic Directional Navigation](../examples/ui/auto_directional_navigation.rs) | Demonstration of automatic directional navigation graph generation based on UI element positions [Borders](../examples/ui/borders.rs) | Demonstrates how to create a node with a border [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 +[Directional Navigation](../examples/ui/navigation/directional_navigation.rs) | Demonstration of automatic directional navigation based on UI element positions +[Directional Navigation Overrides](../examples/ui/navigation/directional_navigation_overrides.rs) | Demonstration of automatic directional navigation between UI elements with manual overrides [Display and Visibility](../examples/ui/display_and_visibility.rs) | Demonstrates how Display and Visibility work in the UI. [Drag to Scroll](../examples/ui/drag_to_scroll.rs) | This example tests scale factor, dragging and scrolling [Feathers Widgets](../examples/ui/feathers.rs) | Gallery of Feathers Widgets diff --git a/examples/ui/directional_navigation.rs b/examples/ui/directional_navigation.rs deleted file mode 100644 index 9592ef984743a..0000000000000 --- a/examples/ui/directional_navigation.rs +++ /dev/null @@ -1,411 +0,0 @@ -//! 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. -//! -//! **Note:** This example shows manual graph construction for full control. For automatic graph generation -//! based on node positions, see the `auto_directional_navigation` example. - -use std::time::Duration; - -use bevy::{ - camera::NormalizedRenderTarget, - input_focus::{ - directional_navigation::{ - DirectionalNavigation, DirectionalNavigationMap, DirectionalNavigationPlugin, - }, - InputDispatchPlugin, InputFocus, InputFocusVisible, - }, - math::CompassOctant, - picking::{ - backend::HitData, - pointer::{Location, PointerId}, - }, - platform::collections::{HashMap, HashSet}, - prelude::*, -}; - -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( - mut click: On>, - mut button_query: Query<(&mut BackgroundColor, &mut ResetTimer)>, -) { - let button_entity = click.entity; - 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 - click.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