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}