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