Skip to main content

bevy_ui/
auto_directional_navigation.rs

1//! An automatic directional navigation system, powered by the [`AutoDirectionalNavigation`] component.
2//!
3//! [`AutoDirectionalNavigator`] expands on the manual directional navigation system
4//! provided by the [`DirectionalNavigation`] system parameter from `bevy_input_focus`.
5
6use crate::{ComputedNode, ComputedUiTargetCamera, UiGlobalTransform};
7use bevy_camera::visibility::InheritedVisibility;
8use bevy_ecs::{prelude::*, system::SystemParam};
9use bevy_math::{ops, CompassOctant, Vec2};
10
11use bevy_input_focus::{
12    directional_navigation::{
13        AutoNavigationConfig, DirectionalNavigation, DirectionalNavigationError, FocusableArea,
14    },
15    navigator::find_best_candidate,
16};
17
18use bevy_reflect::{prelude::*, Reflect};
19
20/// Marker component to enable automatic directional navigation to and from the entity.
21///
22/// Simply add this component to your UI entities so that the navigation algorithm will
23/// consider this entity in its calculations:
24///
25/// ```rust
26/// # use bevy_ecs::prelude::*;
27/// # use bevy_ui::auto_directional_navigation::AutoDirectionalNavigation;
28/// fn spawn_auto_nav_button(mut commands: Commands) {
29///     commands.spawn((
30///         // ... Button, Node, etc. ...
31///         AutoDirectionalNavigation::default(), // That's it!
32///     ));
33/// }
34/// ```
35///
36/// # Multi-Layer UIs and Z-Index
37///
38/// **Important**: Automatic navigation is currently **z-index agnostic** and treats
39/// all entities with `AutoDirectionalNavigation` as a flat set, regardless of which UI layer
40/// or z-index they belong to. This means navigation may jump between different layers (e.g.,
41/// from a background menu to an overlay popup).
42///
43/// **Workarounds** for multi-layer UIs:
44///
45/// 1. **Per-layer manual edge generation**: Query entities by layer and call
46///    [`auto_generate_navigation_edges()`](bevy_input_focus::directional_navigation::auto_generate_navigation_edges)
47///    separately for each layer:
48///    ```rust,ignore
49///    for layer in &layers {
50///        let nodes: Vec<FocusableArea> = query_layer(layer).collect();
51///        auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
52///    }
53///    ```
54///
55/// 2. **Manual cross-layer navigation**: Use
56///    [`DirectionalNavigationMap::add_edge()`](bevy_input_focus::directional_navigation::DirectionalNavigationMap::add_edge)
57///    to define explicit connections between layers (e.g., "Back" button to main menu).
58///
59/// 3. **Remove component when layer is hidden**: Dynamically add/remove
60///    [`AutoDirectionalNavigation`] based on which layers are currently active.
61///
62/// See issue [#21679](https://github.com/bevyengine/bevy/issues/21679) for planned
63/// improvements to layer-aware automatic navigation.
64///
65/// # Opting Out
66///
67/// To disable automatic navigation for specific entities:
68///
69/// - **Remove the component**: Simply don't add [`AutoDirectionalNavigation`] to entities
70///   that should only use manual navigation edges.
71/// - **Dynamically toggle**: Remove/insert the component at runtime to enable/disable
72///   automatic navigation as needed.
73///
74/// Manual edges defined via [`DirectionalNavigationMap`](bevy_input_focus::directional_navigation::DirectionalNavigationMap)
75/// are completely independent and will continue to work regardless of this component.
76///
77/// # Additional Requirements
78///
79/// Entities must also have:
80/// - [`ComputedNode`] - for size information
81/// - [`UiGlobalTransform`] - for position information
82///
83/// These are automatically added by `bevy_ui` when you spawn UI entities.
84///
85/// # Custom UI Systems
86///
87/// For custom UI frameworks, you can call
88/// [`auto_generate_navigation_edges`](bevy_input_focus::directional_navigation::auto_generate_navigation_edges)
89/// directly in your own system instead of using this component.
90#[derive(Component, Default, Debug, Clone, Copy, PartialEq, Reflect)]
91#[reflect(Component, Default, Debug, PartialEq, Clone)]
92pub struct AutoDirectionalNavigation {
93    /// Whether to also consider `TabIndex` for navigation order hints.
94    /// Currently unused but reserved for future functionality.
95    pub respect_tab_order: bool,
96}
97
98/// A system parameter for combining manual and auto navigation between focusable entities in a directional way.
99/// This wraps the [`DirectionalNavigation`] system parameter provided by `bevy_input_focus` and
100/// augments it with auto directional navigation.
101/// To use, the [`DirectionalNavigationPlugin`](bevy_input_focus::directional_navigation::DirectionalNavigationPlugin)
102/// must be added to the app.
103#[derive(SystemParam, Debug)]
104pub struct AutoDirectionalNavigator<'w, 's> {
105    /// A system parameter for the manual directional navigation system provided by `bevy_input_focus`
106    pub manual_directional_navigation: DirectionalNavigation<'w>,
107    /// Configuration for the automated portion of the navigation algorithm.
108    pub config: Res<'w, AutoNavigationConfig>,
109    /// The entities which can possibly be navigated to automatically.
110    navigable_entities_query: Query<
111        'w,
112        's,
113        (
114            Entity,
115            &'static ComputedUiTargetCamera,
116            &'static ComputedNode,
117            &'static UiGlobalTransform,
118            &'static InheritedVisibility,
119        ),
120        With<AutoDirectionalNavigation>,
121    >,
122    /// A query used to get the target camera and the [`FocusableArea`] for a given entity to be used in automatic navigation.
123    camera_and_focusable_area_query: Query<
124        'w,
125        's,
126        (
127            Entity,
128            &'static ComputedUiTargetCamera,
129            &'static ComputedNode,
130            &'static UiGlobalTransform,
131        ),
132        With<AutoDirectionalNavigation>,
133    >,
134}
135
136impl<'w, 's> AutoDirectionalNavigator<'w, 's> {
137    /// Returns the current input focus
138    pub fn input_focus(&mut self) -> Option<Entity> {
139        self.manual_directional_navigation.focus.0
140    }
141
142    /// Tries to find the neighbor in a given direction from the given entity. Assumes the entity is valid.
143    ///
144    /// Returns a neighbor if successful.
145    /// Returns None if there is no neighbor in the requested direction.
146    pub fn navigate(
147        &mut self,
148        direction: CompassOctant,
149    ) -> Result<Entity, DirectionalNavigationError> {
150        if let Some(current_focus) = self.input_focus() {
151            // Respect manual edges first
152            if let Ok(new_focus) = self.manual_directional_navigation.navigate(direction) {
153                self.manual_directional_navigation.focus.set(new_focus);
154                Ok(new_focus)
155            } else if let Some((target_camera, origin)) =
156                self.entity_to_camera_and_focusable_area(current_focus)
157                && let Some(new_focus) = find_best_candidate(
158                    &origin,
159                    direction,
160                    &self.get_navigable_nodes(target_camera),
161                    &self.config,
162                )
163            {
164                self.manual_directional_navigation.focus.set(new_focus);
165                Ok(new_focus)
166            } else {
167                Err(DirectionalNavigationError::NoNeighborInDirection {
168                    current_focus,
169                    direction,
170                })
171            }
172        } else {
173            Err(DirectionalNavigationError::NoFocus)
174        }
175    }
176
177    /// Returns a vec of [`FocusableArea`] representing nodes that are eligible to be automatically navigated to.
178    /// The camera of any navigable nodes will equal the desired `target_camera`.
179    fn get_navigable_nodes(&self, target_camera: Entity) -> Vec<FocusableArea> {
180        self.navigable_entities_query
181            .iter()
182            .filter_map(
183                |(entity, computed_target_camera, computed, transform, inherited_visibility)| {
184                    // Skip hidden or zero-size nodes
185                    if computed.is_empty() || !inherited_visibility.get() {
186                        return None;
187                    }
188                    // Accept nodes that have the same target camera as the desired target camera
189                    if let Some(tc) = computed_target_camera.get()
190                        && tc == target_camera
191                    {
192                        let (scale, rotation, translation) = transform.to_scale_angle_translation();
193                        let scaled_size = computed.size() * computed.inverse_scale_factor() * scale;
194                        let rotated_size = get_rotated_bounds(scaled_size, rotation);
195                        Some(FocusableArea {
196                            entity,
197                            position: translation * computed.inverse_scale_factor(),
198                            size: rotated_size,
199                        })
200                    } else {
201                        // The node either does not have a target camera or it is not the same as the desired one.
202                        None
203                    }
204                },
205            )
206            .collect()
207    }
208
209    /// Gets the target camera and the [`FocusableArea`] of the provided entity, if it exists.
210    ///
211    /// Returns None if there was a [`QueryEntityError`](bevy_ecs::query::QueryEntityError) or
212    /// if the entity does not have a target camera.
213    fn entity_to_camera_and_focusable_area(
214        &self,
215        entity: Entity,
216    ) -> Option<(Entity, FocusableArea)> {
217        self.camera_and_focusable_area_query.get(entity).map_or(
218            None,
219            |(entity, computed_target_camera, computed, transform)| {
220                if let Some(target_camera) = computed_target_camera.get() {
221                    let (scale, rotation, translation) = transform.to_scale_angle_translation();
222                    let scaled_size = computed.size() * computed.inverse_scale_factor() * scale;
223                    let rotated_size = get_rotated_bounds(scaled_size, rotation);
224                    Some((
225                        target_camera,
226                        FocusableArea {
227                            entity,
228                            position: translation * computed.inverse_scale_factor(),
229                            size: rotated_size,
230                        },
231                    ))
232                } else {
233                    None
234                }
235            },
236        )
237    }
238}
239
240fn get_rotated_bounds(size: Vec2, rotation: f32) -> Vec2 {
241    if rotation == 0.0 {
242        return size;
243    }
244    let cos_r = ops::cos(rotation).abs();
245    let sin_r = ops::sin(rotation).abs();
246    Vec2::new(
247        size.x * cos_r + size.y * sin_r,
248        size.x * sin_r + size.y * cos_r,
249    )
250}