bevy_input_focus/directional_navigation.rs
1//! A navigation framework for moving between focusable elements based on directional input.
2//!
3//! While virtual cursors are a common way to navigate UIs with a gamepad (or arrow keys!),
4//! they are generally both slow and frustrating to use.
5//! Instead, directional inputs should provide a direct way to snap between focusable elements.
6//!
7//! Like the rest of this crate, the [`InputFocus`] resource is manipulated to track
8//! the current focus.
9//!
10//! Navigating between focusable entities (commonly UI nodes) is done by
11//! passing a [`CompassOctant`] into the [`navigate`](DirectionalNavigation::navigate) method
12//! from the [`DirectionalNavigation`] system parameter. Under the hood, an entity is found
13//! automatically via brute force search in the desired [`CompassOctant`] direction.
14//!
15//! If some manual navigation is desired, a [`DirectionalNavigationMap`] will override the brute force
16//! search in a direction for a given entity. The [`DirectionalNavigationMap`] stores a directed graph
17//! of focusable entities. Each entity can have up to 8 neighbors, one for each [`CompassOctant`],
18//! balancing flexibility and required precision.
19//!
20//! # Setting up Directional Navigation
21//!
22//! ## Automatic Navigation (Recommended)
23//!
24//! The easiest way to set up navigation is to add the [`AutoDirectionalNavigation`] component
25//! to your UI entities. The system will automatically compute the nearest neighbor in each direction
26//! based on position and size:
27//!
28//! ```rust,no_run
29//! # use bevy_ecs::prelude::*;
30//! # use bevy_input_focus::directional_navigation::AutoDirectionalNavigation;
31//! # use bevy_ui::Node;
32//! fn spawn_button(mut commands: Commands) {
33//! commands.spawn((
34//! Node::default(),
35//! // ... other UI components ...
36//! AutoDirectionalNavigation::default(), // That's it!
37//! ));
38//! }
39//! ```
40//!
41//! ## Manual Navigation
42//!
43//! You can also manually define navigation connections using methods like
44//! [`add_edge`](DirectionalNavigationMap::add_edge) and
45//! [`add_looping_edges`](DirectionalNavigationMap::add_looping_edges).
46//!
47//! ## Combining Automatic and Manual
48//!
49//! Following manual edges always take precedence, allowing you to use
50//! automatic navigation for most UI elements while overriding specific connections for
51//! special cases like wrapping menus or cross-layer navigation.
52//!
53//! ## When to Use Manual Navigation
54//!
55//! While automatic navigation is recommended for most use cases, manual navigation provides:
56//!
57//! - **Precise control**: Define exact navigation flow, including non-obvious connections like looping edges
58//! - **Cross-layer navigation**: Connect elements across different UI layers or z-index levels
59//! - **Custom behavior**: Implement domain-specific navigation patterns (e.g., spreadsheet-style wrapping)
60
61use alloc::vec::Vec;
62use bevy_app::prelude::*;
63use bevy_camera::visibility::InheritedVisibility;
64use bevy_ecs::{
65 entity::{EntityHashMap, EntityHashSet},
66 prelude::*,
67 system::SystemParam,
68};
69use bevy_math::{CompassOctant, Dir2, Vec2};
70use bevy_ui::{ComputedNode, UiGlobalTransform};
71use thiserror::Error;
72
73use crate::InputFocus;
74
75#[cfg(feature = "bevy_reflect")]
76use bevy_reflect::{prelude::*, Reflect};
77
78/// A plugin that sets up the directional navigation resources.
79#[derive(Default)]
80pub struct DirectionalNavigationPlugin;
81
82impl Plugin for DirectionalNavigationPlugin {
83 fn build(&self, app: &mut App) {
84 app.init_resource::<DirectionalNavigationMap>()
85 .init_resource::<AutoNavigationConfig>();
86 }
87}
88
89/// Marker component to enable automatic directional navigation to and from the entity.
90///
91/// Simply add this component to your UI entities so that the navigation algorithm will
92/// consider this entity in its calculations:
93///
94/// ```rust
95/// # use bevy_ecs::prelude::*;
96/// # use bevy_input_focus::directional_navigation::AutoDirectionalNavigation;
97/// fn spawn_auto_nav_button(mut commands: Commands) {
98/// commands.spawn((
99/// // ... Button, Node, etc. ...
100/// AutoDirectionalNavigation::default(), // That's it!
101/// ));
102/// }
103/// ```
104///
105/// # Multi-Layer UIs and Z-Index
106///
107/// **Important**: Automatic navigation is currently **z-index agnostic** and treats
108/// all entities with `AutoDirectionalNavigation` as a flat set, regardless of which UI layer
109/// or z-index they belong to. This means navigation may jump between different layers (e.g.,
110/// from a background menu to an overlay popup).
111///
112/// **Workarounds** for multi-layer UIs:
113///
114/// 1. **Per-layer manual edge generation**: Query entities by layer and call
115/// [`auto_generate_navigation_edges()`] separately for each layer:
116/// ```rust,ignore
117/// for layer in &layers {
118/// let nodes: Vec<FocusableArea> = query_layer(layer).collect();
119/// auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
120/// }
121/// ```
122///
123/// 2. **Manual cross-layer navigation**: Use [`DirectionalNavigationMap::add_edge()`]
124/// to define explicit connections between layers (e.g., "Back" button to main menu).
125///
126/// 3. **Remove component when layer is hidden**: Dynamically add/remove
127/// `AutoDirectionalNavigation` based on which layers are currently active.
128///
129/// See issue [#21679](https://github.com/bevyengine/bevy/issues/21679) for planned
130/// improvements to layer-aware automatic navigation.
131///
132/// # Opting Out
133///
134/// To disable automatic navigation for specific entities:
135///
136/// - **Remove the component**: Simply don't add `AutoDirectionalNavigation` to entities
137/// that should only use manual navigation edges.
138/// - **Dynamically toggle**: Remove/insert the component at runtime to enable/disable
139/// automatic navigation as needed.
140///
141/// Manual edges defined via [`DirectionalNavigationMap`] are completely independent and
142/// will continue to work regardless of this component.
143///
144/// # Requirements (for `bevy_ui`)
145///
146/// Entities must also have:
147/// - [`ComputedNode`] - for size information
148/// - [`UiGlobalTransform`] - for position information
149///
150/// These are automatically added by `bevy_ui` when you spawn UI entities.
151///
152/// # Custom UI Systems
153///
154/// For custom UI frameworks, you can call [`auto_generate_navigation_edges`] directly
155/// in your own system instead of using this component.
156#[derive(Component, Default, Debug, Clone, Copy, PartialEq)]
157#[cfg_attr(
158 feature = "bevy_reflect",
159 derive(Reflect),
160 reflect(Component, Default, Debug, PartialEq, Clone)
161)]
162pub struct AutoDirectionalNavigation {
163 /// Whether to also consider `TabIndex` for navigation order hints.
164 /// Currently unused but reserved for future functionality.
165 pub respect_tab_order: bool,
166}
167
168/// Configuration resource for automatic navigation.
169///
170/// This resource controls how the automatic navigation system computes which
171/// nodes should be connected in each direction.
172#[derive(Resource, Debug, Clone, PartialEq)]
173#[cfg_attr(
174 feature = "bevy_reflect",
175 derive(Reflect),
176 reflect(Resource, Debug, PartialEq, Clone)
177)]
178pub struct AutoNavigationConfig {
179 /// Minimum overlap ratio (0.0-1.0) required along the perpendicular axis for cardinal directions.
180 ///
181 /// This parameter controls how much two UI elements must overlap in the perpendicular direction
182 /// to be considered reachable neighbors. It only applies to cardinal directions (`North`, `South`, `East`, `West`);
183 /// diagonal directions (`NorthEast`, `SouthEast`, etc.) ignore this requirement entirely.
184 ///
185 /// # Calculation
186 ///
187 /// The overlap factor is calculated as:
188 /// ```text
189 /// overlap_factor = actual_overlap / min(origin_size, candidate_size)
190 /// ```
191 ///
192 /// For East/West navigation, this measures vertical overlap:
193 /// - `actual_overlap` = overlapping height between the two elements
194 /// - Sizes are the heights of the origin and candidate
195 ///
196 /// For North/South navigation, this measures horizontal overlap:
197 /// - `actual_overlap` = overlapping width between the two elements
198 /// - Sizes are the widths of the origin and candidate
199 ///
200 /// # Examples
201 ///
202 /// - `0.0` (default): Any overlap is sufficient. Even if elements barely touch, they can be neighbors.
203 /// - `0.5`: Elements must overlap by at least 50% of the smaller element's size.
204 /// - `1.0`: Perfect alignment required. The smaller element must be completely within the bounds
205 /// of the larger element along the perpendicular axis.
206 ///
207 /// # Use Cases
208 ///
209 /// - **Sparse/irregular layouts** (e.g., star constellations): Use `0.0` to allow navigation
210 /// between elements that don't directly align.
211 /// - **Grid layouts**: Use `0.5` or higher to ensure navigation only connects elements in
212 /// the same row or column.
213 /// - **Strict alignment**: Use `1.0` to require perfect alignment, though this may result
214 /// in disconnected navigation graphs if elements aren't precisely aligned.
215 pub min_alignment_factor: f32,
216
217 /// Maximum search distance in logical pixels.
218 ///
219 /// Nodes beyond this distance won't be connected. `None` means unlimited.
220 pub max_search_distance: Option<f32>,
221
222 /// Whether to prefer nodes that are more aligned with the exact direction.
223 ///
224 /// When `true`, nodes that are more directly in line with the requested direction
225 /// will be strongly preferred over nodes at an angle.
226 pub prefer_aligned: bool,
227}
228
229impl Default for AutoNavigationConfig {
230 fn default() -> Self {
231 Self {
232 min_alignment_factor: 0.0, // Any overlap is acceptable
233 max_search_distance: None, // No distance limit
234 prefer_aligned: true, // Prefer well-aligned nodes
235 }
236 }
237}
238
239/// The up-to-eight neighbors of a focusable entity, one for each [`CompassOctant`].
240#[derive(Default, Debug, Clone, PartialEq)]
241#[cfg_attr(
242 feature = "bevy_reflect",
243 derive(Reflect),
244 reflect(Default, Debug, PartialEq, Clone)
245)]
246pub struct NavNeighbors {
247 /// The array of neighbors, one for each [`CompassOctant`].
248 /// The mapping between array elements and directions is determined by [`CompassOctant::to_index`].
249 ///
250 /// If no neighbor exists in a given direction, the value will be [`None`].
251 /// In most cases, using [`NavNeighbors::set`] and [`NavNeighbors::get`]
252 /// will be more ergonomic than directly accessing this array.
253 pub neighbors: [Option<Entity>; 8],
254}
255
256impl NavNeighbors {
257 /// An empty set of neighbors.
258 pub const EMPTY: NavNeighbors = NavNeighbors {
259 neighbors: [None; 8],
260 };
261
262 /// Get the neighbor for a given [`CompassOctant`].
263 pub const fn get(&self, octant: CompassOctant) -> Option<Entity> {
264 self.neighbors[octant.to_index()]
265 }
266
267 /// Set the neighbor for a given [`CompassOctant`].
268 pub const fn set(&mut self, octant: CompassOctant, entity: Entity) {
269 self.neighbors[octant.to_index()] = Some(entity);
270 }
271}
272
273/// A resource that stores the manually specified traversable graph of focusable entities.
274///
275/// Each entity can have up to 8 neighbors, one for each [`CompassOctant`].
276///
277/// To ensure that your graph is intuitive to navigate and generally works correctly, it should be:
278///
279/// - **Connected**: Every focusable entity should be reachable from every other focusable entity.
280/// - **Symmetric**: If entity A is a neighbor of entity B, then entity B should be a neighbor of entity A, ideally in the reverse direction.
281/// - **Physical**: The direction of navigation should match the layout of the entities when possible,
282/// although looping around the edges of the screen is also acceptable.
283/// - **Not self-connected**: An entity should not be a neighbor of itself; use [`None`] instead.
284///
285/// This graph must be built and maintained manually, and the developer is responsible for ensuring that it meets the above criteria.
286/// Notably, if the developer adds or removes the navigability of an entity, the developer should update the map as necessary.
287#[derive(Resource, Debug, Default, Clone, PartialEq)]
288#[cfg_attr(
289 feature = "bevy_reflect",
290 derive(Reflect),
291 reflect(Resource, Debug, Default, PartialEq, Clone)
292)]
293pub struct DirectionalNavigationMap {
294 /// A directed graph of focusable entities.
295 ///
296 /// Pass in the current focus as a key, and get back a collection of up to 8 neighbors,
297 /// each keyed by a [`CompassOctant`].
298 pub neighbors: EntityHashMap<NavNeighbors>,
299}
300
301impl DirectionalNavigationMap {
302 /// Removes an entity from the navigation map, including all connections to and from it.
303 ///
304 /// Note that this is an O(n) operation, where n is the number of entities in the map,
305 /// as we must iterate over each entity to check for connections to the removed entity.
306 ///
307 /// If you are removing multiple entities, consider using [`remove_multiple`](Self::remove_multiple) instead.
308 pub fn remove(&mut self, entity: Entity) {
309 self.neighbors.remove(&entity);
310
311 for node in self.neighbors.values_mut() {
312 for neighbor in node.neighbors.iter_mut() {
313 if *neighbor == Some(entity) {
314 *neighbor = None;
315 }
316 }
317 }
318 }
319
320 /// Removes a collection of entities from the navigation map.
321 ///
322 /// While this is still an O(n) operation, where n is the number of entities in the map,
323 /// it is more efficient than calling [`remove`](Self::remove) multiple times,
324 /// as we can check for connections to all removed entities in a single pass.
325 ///
326 /// An [`EntityHashSet`] must be provided as it is noticeably faster than the standard hasher or a [`Vec`](`alloc::vec::Vec`).
327 pub fn remove_multiple(&mut self, entities: EntityHashSet) {
328 for entity in &entities {
329 self.neighbors.remove(entity);
330 }
331
332 for node in self.neighbors.values_mut() {
333 for neighbor in node.neighbors.iter_mut() {
334 if let Some(entity) = *neighbor {
335 if entities.contains(&entity) {
336 *neighbor = None;
337 }
338 }
339 }
340 }
341 }
342
343 /// Completely clears the navigation map, removing all entities and connections.
344 pub fn clear(&mut self) {
345 self.neighbors.clear();
346 }
347
348 /// Adds an edge between two entities in the navigation map.
349 /// Any existing edge from A in the provided direction will be overwritten.
350 ///
351 /// The reverse edge will not be added, so navigation will only be possible in one direction.
352 /// If you want to add a symmetrical edge, use [`add_symmetrical_edge`](Self::add_symmetrical_edge) instead.
353 pub fn add_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
354 self.neighbors
355 .entry(a)
356 .or_insert(NavNeighbors::EMPTY)
357 .set(direction, b);
358 }
359
360 /// Adds a symmetrical edge between two entities in the navigation map.
361 /// The A -> B path will use the provided direction, while B -> A will use the [`CompassOctant::opposite`] variant.
362 ///
363 /// Any existing connections between the two entities will be overwritten.
364 pub fn add_symmetrical_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
365 self.add_edge(a, b, direction);
366 self.add_edge(b, a, direction.opposite());
367 }
368
369 /// Add symmetrical edges between each consecutive pair of entities in the provided slice.
370 ///
371 /// Unlike [`add_looping_edges`](Self::add_looping_edges), this method does not loop back to the first entity.
372 pub fn add_edges(&mut self, entities: &[Entity], direction: CompassOctant) {
373 for pair in entities.windows(2) {
374 self.add_symmetrical_edge(pair[0], pair[1], direction);
375 }
376 }
377
378 /// Add symmetrical edges between each consecutive pair of entities in the provided slice, looping back to the first entity at the end.
379 ///
380 /// This is useful for creating a circular navigation path between a set of entities, such as a menu.
381 pub fn add_looping_edges(&mut self, entities: &[Entity], direction: CompassOctant) {
382 self.add_edges(entities, direction);
383 if let Some((first_entity, rest)) = entities.split_first() {
384 if let Some(last_entity) = rest.last() {
385 self.add_symmetrical_edge(*last_entity, *first_entity, direction);
386 }
387 }
388 }
389
390 /// Gets the entity in a given direction from the current focus, if any.
391 pub fn get_neighbor(&self, focus: Entity, octant: CompassOctant) -> Option<Entity> {
392 self.neighbors
393 .get(&focus)
394 .and_then(|neighbors| neighbors.get(octant))
395 }
396
397 /// Looks up the neighbors of a given entity.
398 ///
399 /// If the entity is not in the map, [`None`] will be returned.
400 /// Note that the set of neighbors is not guaranteed to be non-empty though!
401 pub fn get_neighbors(&self, entity: Entity) -> Option<&NavNeighbors> {
402 self.neighbors.get(&entity)
403 }
404}
405
406/// A system parameter for navigating between focusable entities in a directional way.
407#[derive(SystemParam, Debug)]
408pub struct DirectionalNavigation<'w, 's> {
409 /// The currently focused entity.
410 pub focus: ResMut<'w, InputFocus>,
411 /// The directional navigation map containing manually defined connections between entities.
412 pub map: Res<'w, DirectionalNavigationMap>,
413 /// Configuration for the automated portion of the navigation algorithm.
414 pub config: Res<'w, AutoNavigationConfig>,
415 /// The entities which can possibly be navigated to automatically.
416 navigable_entities_query: Query<
417 'w,
418 's,
419 (
420 Entity,
421 &'static ComputedNode,
422 &'static UiGlobalTransform,
423 &'static InheritedVisibility,
424 ),
425 With<AutoDirectionalNavigation>,
426 >,
427 /// A query used to get the [`FocusableArea`] for a given entity to be used in automatic navigation.
428 focusable_area_query: Query<
429 'w,
430 's,
431 (Entity, &'static ComputedNode, &'static UiGlobalTransform),
432 With<AutoDirectionalNavigation>,
433 >,
434}
435
436impl<'w, 's> DirectionalNavigation<'w, 's> {
437 /// Navigates to the neighbor in a given direction from the current focus, if any.
438 ///
439 /// Returns the new focus if successful.
440 /// Returns an error if there is no focus set or if there is no neighbor in the requested direction.
441 ///
442 /// If the result was `Ok`, the [`InputFocus`] resource is updated to the new focus as part of this method call.
443 pub fn navigate(
444 &mut self,
445 direction: CompassOctant,
446 ) -> Result<Entity, DirectionalNavigationError> {
447 if let Some(current_focus) = self.focus.0 {
448 // Respect manual edges first
449 if let Some(new_focus) = self.map.get_neighbor(current_focus, direction) {
450 self.focus.set(new_focus);
451 Ok(new_focus)
452 } else if let Some(origin) = self.entity_to_focusable_area(current_focus)
453 && let Some(new_focus) = find_best_candidate(
454 &origin,
455 direction,
456 &self.get_navigable_nodes(),
457 &self.config,
458 )
459 {
460 self.focus.set(new_focus);
461 Ok(new_focus)
462 } else {
463 Err(DirectionalNavigationError::NoNeighborInDirection {
464 current_focus,
465 direction,
466 })
467 }
468 } else {
469 Err(DirectionalNavigationError::NoFocus)
470 }
471 }
472
473 /// Returns a vec of [`FocusableArea`] representing nodes that are eligible to be automatically navigated to.
474 fn get_navigable_nodes(&self) -> Vec<FocusableArea> {
475 self.navigable_entities_query
476 .iter()
477 .filter_map(|(entity, computed, transform, inherited_visibility)| {
478 // Skip hidden or zero-size nodes
479 if computed.is_empty() || !inherited_visibility.get() {
480 return None;
481 }
482
483 let (_scale, _rotation, translation) = transform.to_scale_angle_translation();
484 Some(FocusableArea {
485 entity,
486 position: translation,
487 size: computed.size(),
488 })
489 })
490 .collect()
491 }
492
493 /// Gets the [`FocusableArea`] of the provided entity, if it exists.
494 ///
495 /// Returns None if there was a [`QueryEntityError`](bevy_ecs::query::QueryEntityError).
496 fn entity_to_focusable_area(&self, entity: Entity) -> Option<FocusableArea> {
497 self.focusable_area_query
498 .get(entity)
499 .map_or(None, |(entity, computed, transform)| {
500 let (_scale, _rotation, translation) = transform.to_scale_angle_translation();
501 Some(FocusableArea {
502 entity,
503 position: translation,
504 size: computed.size(),
505 })
506 })
507 }
508}
509
510/// An error that can occur when navigating between focusable entities using [directional navigation](crate::directional_navigation).
511#[derive(Debug, PartialEq, Clone, Error)]
512pub enum DirectionalNavigationError {
513 /// No focusable entity is currently set.
514 #[error("No focusable entity is currently set.")]
515 NoFocus,
516 /// No neighbor in the requested direction.
517 #[error("No neighbor from {current_focus} in the {direction:?} direction.")]
518 NoNeighborInDirection {
519 /// The entity that was the focus when the error occurred.
520 current_focus: Entity,
521 /// The direction in which the navigation was attempted.
522 direction: CompassOctant,
523 },
524}
525
526/// A focusable area with position and size information.
527///
528/// This struct represents a UI element used during automatic directional navigation,
529/// containing its entity ID, center position, and size for spatial navigation calculations.
530///
531/// The term "focusable area" avoids confusion with UI [`Node`](bevy_ui::Node) components.
532#[derive(Debug, Clone, Copy, PartialEq)]
533#[cfg_attr(
534 feature = "bevy_reflect",
535 derive(Reflect),
536 reflect(Debug, PartialEq, Clone)
537)]
538pub struct FocusableArea {
539 /// The entity identifier for this focusable area.
540 pub entity: Entity,
541 /// The center position in global coordinates.
542 pub position: Vec2,
543 /// The size (width, height) of the area.
544 pub size: Vec2,
545}
546
547/// Trait for extracting position and size from navigable UI components.
548///
549/// This allows the auto-navigation system to work with different UI implementations
550/// as long as they can provide position and size information.
551pub trait Navigable {
552 /// Returns the center position and size in global coordinates.
553 fn get_bounds(&self) -> (Vec2, Vec2);
554}
555
556// We can't directly implement this for `bevy_ui` types here without circular dependencies,
557// so we'll use a more generic approach with separate functions for different component sets.
558
559/// Calculate 1D overlap between two ranges.
560///
561/// Returns a value between 0.0 (no overlap) and 1.0 (perfect overlap).
562fn calculate_1d_overlap(
563 origin_pos: f32,
564 origin_size: f32,
565 candidate_pos: f32,
566 candidate_size: f32,
567) -> f32 {
568 let origin_min = origin_pos - origin_size / 2.0;
569 let origin_max = origin_pos + origin_size / 2.0;
570 let cand_min = candidate_pos - candidate_size / 2.0;
571 let cand_max = candidate_pos + candidate_size / 2.0;
572
573 let overlap = (origin_max.min(cand_max) - origin_min.max(cand_min)).max(0.0);
574 let max_overlap = origin_size.min(candidate_size);
575 if max_overlap > 0.0 {
576 overlap / max_overlap
577 } else {
578 0.0
579 }
580}
581
582/// Calculate the overlap factor between two nodes in the perpendicular axis.
583///
584/// Returns a value between 0.0 (no overlap) and 1.0 (perfect overlap).
585/// For diagonal directions, always returns 1.0.
586fn calculate_overlap(
587 origin_pos: Vec2,
588 origin_size: Vec2,
589 candidate_pos: Vec2,
590 candidate_size: Vec2,
591 octant: CompassOctant,
592) -> f32 {
593 match octant {
594 CompassOctant::North | CompassOctant::South => {
595 // Check horizontal overlap
596 calculate_1d_overlap(
597 origin_pos.x,
598 origin_size.x,
599 candidate_pos.x,
600 candidate_size.x,
601 )
602 }
603 CompassOctant::East | CompassOctant::West => {
604 // Check vertical overlap
605 calculate_1d_overlap(
606 origin_pos.y,
607 origin_size.y,
608 candidate_pos.y,
609 candidate_size.y,
610 )
611 }
612 // Diagonal directions don't require strict overlap
613 _ => 1.0,
614 }
615}
616
617/// Score a candidate node for navigation in a given direction.
618///
619/// Lower score is better. Returns `f32::INFINITY` for unreachable nodes.
620fn score_candidate(
621 origin_pos: Vec2,
622 origin_size: Vec2,
623 candidate_pos: Vec2,
624 candidate_size: Vec2,
625 octant: CompassOctant,
626 config: &AutoNavigationConfig,
627) -> f32 {
628 // Get direction in mathematical coordinates, then flip Y for UI coordinates
629 let dir = Dir2::from(octant).as_vec2() * Vec2::new(1.0, -1.0);
630 let to_candidate = candidate_pos - origin_pos;
631 let distance = to_candidate.length();
632
633 // Check direction first
634 // Convert UI coordinates (Y+ = down) to mathematical coordinates (Y+ = up) by flipping Y
635 let origin_math = Vec2::new(origin_pos.x, -origin_pos.y);
636 let candidate_math = Vec2::new(candidate_pos.x, -candidate_pos.y);
637 if !octant.is_in_direction(origin_math, candidate_math) {
638 return f32::INFINITY;
639 }
640
641 // Check overlap for cardinal directions
642 let overlap_factor = calculate_overlap(
643 origin_pos,
644 origin_size,
645 candidate_pos,
646 candidate_size,
647 octant,
648 );
649
650 if overlap_factor < config.min_alignment_factor {
651 return f32::INFINITY;
652 }
653
654 // Check max distance
655 if let Some(max_dist) = config.max_search_distance {
656 if distance > max_dist {
657 return f32::INFINITY;
658 }
659 }
660
661 // Calculate alignment score
662 let alignment = if distance > 0.0 {
663 to_candidate.normalize().dot(dir).max(0.0)
664 } else {
665 1.0
666 };
667
668 // Combine distance and alignment
669 // Prefer aligned nodes by penalizing misalignment
670 let alignment_penalty = if config.prefer_aligned {
671 (1.0 - alignment) * distance * 2.0 // Misalignment scales with distance
672 } else {
673 0.0
674 };
675
676 distance + alignment_penalty
677}
678
679/// Finds the best entity to navigate to from the origin towards the given direction.
680///
681/// For details on what "best" means here, refer to [`AutoNavigationConfig`].
682fn find_best_candidate(
683 origin: &FocusableArea,
684 direction: CompassOctant,
685 candidates: &[FocusableArea],
686 config: &AutoNavigationConfig,
687) -> Option<Entity> {
688 // Find best candidate in this direction
689 let mut best_candidate = None;
690 let mut best_score = f32::INFINITY;
691
692 for candidate in candidates {
693 // Skip self
694 if candidate.entity == origin.entity {
695 continue;
696 }
697
698 // Score the candidate
699 let score = score_candidate(
700 origin.position,
701 origin.size,
702 candidate.position,
703 candidate.size,
704 direction,
705 config,
706 );
707
708 if score < best_score {
709 best_score = score;
710 best_candidate = Some(candidate.entity);
711 }
712 }
713
714 best_candidate
715}
716
717/// Automatically generates directional navigation edges for a collection of nodes.
718///
719/// This function takes a slice of navigation nodes with their positions and sizes, and populates
720/// the navigation map with edges to the nearest neighbor in each compass direction.
721/// Manual edges already in the map are preserved and not overwritten.
722///
723/// # Arguments
724///
725/// * `nav_map` - The navigation map to populate
726/// * `nodes` - A slice of [`FocusableArea`] structs containing entity, position, and size data
727/// * `config` - Configuration for the auto-generation algorithm
728///
729/// # Example
730///
731/// ```rust
732/// # use bevy_input_focus::directional_navigation::*;
733/// # use bevy_ecs::entity::Entity;
734/// # use bevy_math::Vec2;
735/// let mut nav_map = DirectionalNavigationMap::default();
736/// let config = AutoNavigationConfig::default();
737///
738/// let nodes = vec![
739/// FocusableArea { entity: Entity::PLACEHOLDER, position: Vec2::new(100.0, 100.0), size: Vec2::new(50.0, 50.0) },
740/// FocusableArea { entity: Entity::PLACEHOLDER, position: Vec2::new(200.0, 100.0), size: Vec2::new(50.0, 50.0) },
741/// ];
742///
743/// auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
744/// ```
745pub fn auto_generate_navigation_edges(
746 nav_map: &mut DirectionalNavigationMap,
747 nodes: &[FocusableArea],
748 config: &AutoNavigationConfig,
749) {
750 // For each node, find best neighbor in each direction
751 for origin in nodes {
752 for octant in [
753 CompassOctant::North,
754 CompassOctant::NorthEast,
755 CompassOctant::East,
756 CompassOctant::SouthEast,
757 CompassOctant::South,
758 CompassOctant::SouthWest,
759 CompassOctant::West,
760 CompassOctant::NorthWest,
761 ] {
762 // Skip if manual edge already exists (check inline to avoid borrow issues)
763 if nav_map
764 .get_neighbors(origin.entity)
765 .and_then(|neighbors| neighbors.get(octant))
766 .is_some()
767 {
768 continue; // Respect manual override
769 }
770
771 // Find best candidate in this direction
772 let best_candidate = find_best_candidate(origin, octant, nodes, config);
773
774 // Add edge if we found a valid candidate
775 if let Some(neighbor) = best_candidate {
776 nav_map.add_edge(origin.entity, neighbor, octant);
777 }
778 }
779 }
780}
781
782#[cfg(test)]
783mod tests {
784 use alloc::vec;
785 use bevy_ecs::system::RunSystemOnce;
786
787 use super::*;
788
789 #[test]
790 fn setting_and_getting_nav_neighbors() {
791 let mut neighbors = NavNeighbors::EMPTY;
792 assert_eq!(neighbors.get(CompassOctant::SouthEast), None);
793
794 neighbors.set(CompassOctant::SouthEast, Entity::PLACEHOLDER);
795
796 for i in 0..8 {
797 if i == CompassOctant::SouthEast.to_index() {
798 assert_eq!(
799 neighbors.get(CompassOctant::SouthEast),
800 Some(Entity::PLACEHOLDER)
801 );
802 } else {
803 assert_eq!(neighbors.get(CompassOctant::from_index(i).unwrap()), None);
804 }
805 }
806 }
807
808 #[test]
809 fn simple_set_and_get_navmap() {
810 let mut world = World::new();
811 let a = world.spawn_empty().id();
812 let b = world.spawn_empty().id();
813
814 let mut map = DirectionalNavigationMap::default();
815 map.add_edge(a, b, CompassOctant::SouthEast);
816
817 assert_eq!(map.get_neighbor(a, CompassOctant::SouthEast), Some(b));
818 assert_eq!(
819 map.get_neighbor(b, CompassOctant::SouthEast.opposite()),
820 None
821 );
822 }
823
824 #[test]
825 fn symmetrical_edges() {
826 let mut world = World::new();
827 let a = world.spawn_empty().id();
828 let b = world.spawn_empty().id();
829
830 let mut map = DirectionalNavigationMap::default();
831 map.add_symmetrical_edge(a, b, CompassOctant::North);
832
833 assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b));
834 assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a));
835 }
836
837 #[test]
838 fn remove_nodes() {
839 let mut world = World::new();
840 let a = world.spawn_empty().id();
841 let b = world.spawn_empty().id();
842
843 let mut map = DirectionalNavigationMap::default();
844 map.add_edge(a, b, CompassOctant::North);
845 map.add_edge(b, a, CompassOctant::South);
846
847 assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b));
848 assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a));
849
850 map.remove(b);
851
852 assert_eq!(map.get_neighbor(a, CompassOctant::North), None);
853 assert_eq!(map.get_neighbor(b, CompassOctant::South), None);
854 }
855
856 #[test]
857 fn remove_multiple_nodes() {
858 let mut world = World::new();
859 let a = world.spawn_empty().id();
860 let b = world.spawn_empty().id();
861 let c = world.spawn_empty().id();
862
863 let mut map = DirectionalNavigationMap::default();
864 map.add_edge(a, b, CompassOctant::North);
865 map.add_edge(b, a, CompassOctant::South);
866 map.add_edge(b, c, CompassOctant::East);
867 map.add_edge(c, b, CompassOctant::West);
868
869 let mut to_remove = EntityHashSet::default();
870 to_remove.insert(b);
871 to_remove.insert(c);
872
873 map.remove_multiple(to_remove);
874
875 assert_eq!(map.get_neighbor(a, CompassOctant::North), None);
876 assert_eq!(map.get_neighbor(b, CompassOctant::South), None);
877 assert_eq!(map.get_neighbor(b, CompassOctant::East), None);
878 assert_eq!(map.get_neighbor(c, CompassOctant::West), None);
879 }
880
881 #[test]
882 fn edges() {
883 let mut world = World::new();
884 let a = world.spawn_empty().id();
885 let b = world.spawn_empty().id();
886 let c = world.spawn_empty().id();
887
888 let mut map = DirectionalNavigationMap::default();
889 map.add_edges(&[a, b, c], CompassOctant::East);
890
891 assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b));
892 assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c));
893 assert_eq!(map.get_neighbor(c, CompassOctant::East), None);
894
895 assert_eq!(map.get_neighbor(a, CompassOctant::West), None);
896 assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a));
897 assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b));
898 }
899
900 #[test]
901 fn looping_edges() {
902 let mut world = World::new();
903 let a = world.spawn_empty().id();
904 let b = world.spawn_empty().id();
905 let c = world.spawn_empty().id();
906
907 let mut map = DirectionalNavigationMap::default();
908 map.add_looping_edges(&[a, b, c], CompassOctant::East);
909
910 assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b));
911 assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c));
912 assert_eq!(map.get_neighbor(c, CompassOctant::East), Some(a));
913
914 assert_eq!(map.get_neighbor(a, CompassOctant::West), Some(c));
915 assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a));
916 assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b));
917 }
918
919 #[test]
920 fn manual_nav_with_system_param() {
921 let mut world = World::new();
922 let a = world.spawn_empty().id();
923 let b = world.spawn_empty().id();
924 let c = world.spawn_empty().id();
925
926 let mut map = DirectionalNavigationMap::default();
927 map.add_looping_edges(&[a, b, c], CompassOctant::East);
928
929 world.insert_resource(map);
930
931 let mut focus = InputFocus::default();
932 focus.set(a);
933 world.insert_resource(focus);
934
935 let config = AutoNavigationConfig::default();
936 world.insert_resource(config);
937
938 assert_eq!(world.resource::<InputFocus>().get(), Some(a));
939
940 fn navigate_east(mut nav: DirectionalNavigation) {
941 nav.navigate(CompassOctant::East).unwrap();
942 }
943
944 world.run_system_once(navigate_east).unwrap();
945 assert_eq!(world.resource::<InputFocus>().get(), Some(b));
946
947 world.run_system_once(navigate_east).unwrap();
948 assert_eq!(world.resource::<InputFocus>().get(), Some(c));
949
950 world.run_system_once(navigate_east).unwrap();
951 assert_eq!(world.resource::<InputFocus>().get(), Some(a));
952 }
953
954 // Tests for automatic navigation helpers
955 #[test]
956 fn test_is_in_direction() {
957 let origin = Vec2::new(100.0, 100.0);
958
959 // Node to the north (mathematically up) should have larger Y
960 let north_node = Vec2::new(100.0, 150.0);
961 assert!(CompassOctant::North.is_in_direction(origin, north_node));
962 assert!(!CompassOctant::South.is_in_direction(origin, north_node));
963
964 // Node to the south (mathematically down) should have smaller Y
965 let south_node = Vec2::new(100.0, 50.0);
966 assert!(CompassOctant::South.is_in_direction(origin, south_node));
967 assert!(!CompassOctant::North.is_in_direction(origin, south_node));
968
969 // Node to the east should be in East direction
970 let east_node = Vec2::new(150.0, 100.0);
971 assert!(CompassOctant::East.is_in_direction(origin, east_node));
972 assert!(!CompassOctant::West.is_in_direction(origin, east_node));
973
974 // Node to the northeast (mathematically up-right) should have larger Y, larger X
975 let ne_node = Vec2::new(150.0, 150.0);
976 assert!(CompassOctant::NorthEast.is_in_direction(origin, ne_node));
977 assert!(!CompassOctant::SouthWest.is_in_direction(origin, ne_node));
978 }
979
980 #[test]
981 fn test_calculate_overlap_horizontal() {
982 let origin_pos = Vec2::new(100.0, 100.0);
983 let origin_size = Vec2::new(50.0, 50.0);
984
985 // Fully overlapping node to the north
986 let north_pos = Vec2::new(100.0, 200.0);
987 let north_size = Vec2::new(50.0, 50.0);
988 let overlap = calculate_overlap(
989 origin_pos,
990 origin_size,
991 north_pos,
992 north_size,
993 CompassOctant::North,
994 );
995 assert_eq!(overlap, 1.0); // Full overlap
996
997 // Partially overlapping node to the north
998 let north_pos = Vec2::new(110.0, 200.0);
999 let partial_overlap = calculate_overlap(
1000 origin_pos,
1001 origin_size,
1002 north_pos,
1003 north_size,
1004 CompassOctant::North,
1005 );
1006 assert!(partial_overlap > 0.0 && partial_overlap < 1.0);
1007
1008 // No overlap
1009 let north_pos = Vec2::new(200.0, 200.0);
1010 let no_overlap = calculate_overlap(
1011 origin_pos,
1012 origin_size,
1013 north_pos,
1014 north_size,
1015 CompassOctant::North,
1016 );
1017 assert_eq!(no_overlap, 0.0);
1018 }
1019
1020 #[test]
1021 fn test_score_candidate() {
1022 let config = AutoNavigationConfig::default();
1023 let origin_pos = Vec2::new(100.0, 100.0);
1024 let origin_size = Vec2::new(50.0, 50.0);
1025
1026 // Node directly to the north (up on screen = smaller Y)
1027 let north_pos = Vec2::new(100.0, 0.0);
1028 let north_size = Vec2::new(50.0, 50.0);
1029 let north_score = score_candidate(
1030 origin_pos,
1031 origin_size,
1032 north_pos,
1033 north_size,
1034 CompassOctant::North,
1035 &config,
1036 );
1037 assert!(north_score < f32::INFINITY);
1038 assert!(north_score < 150.0); // Should be close to the distance (100)
1039
1040 // Node in opposite direction (should be unreachable)
1041 let south_pos = Vec2::new(100.0, 200.0);
1042 let south_size = Vec2::new(50.0, 50.0);
1043 let invalid_score = score_candidate(
1044 origin_pos,
1045 origin_size,
1046 south_pos,
1047 south_size,
1048 CompassOctant::North,
1049 &config,
1050 );
1051 assert_eq!(invalid_score, f32::INFINITY);
1052
1053 // Closer node should have better score than farther node
1054 let close_pos = Vec2::new(100.0, 50.0);
1055 let far_pos = Vec2::new(100.0, -100.0);
1056 let close_score = score_candidate(
1057 origin_pos,
1058 origin_size,
1059 close_pos,
1060 north_size,
1061 CompassOctant::North,
1062 &config,
1063 );
1064 let far_score = score_candidate(
1065 origin_pos,
1066 origin_size,
1067 far_pos,
1068 north_size,
1069 CompassOctant::North,
1070 &config,
1071 );
1072 assert!(close_score < far_score);
1073 }
1074
1075 #[test]
1076 fn test_auto_generate_navigation_edges() {
1077 let mut nav_map = DirectionalNavigationMap::default();
1078 let config = AutoNavigationConfig::default();
1079
1080 // Create a 2x2 grid of nodes (using UI coordinates: smaller Y = higher on screen)
1081 let node_a = Entity::from_bits(1); // Top-left
1082 let node_b = Entity::from_bits(2); // Top-right
1083 let node_c = Entity::from_bits(3); // Bottom-left
1084 let node_d = Entity::from_bits(4); // Bottom-right
1085
1086 let nodes = vec![
1087 FocusableArea {
1088 entity: node_a,
1089 position: Vec2::new(0.0, 0.0),
1090 size: Vec2::new(50.0, 50.0),
1091 }, // Top-left
1092 FocusableArea {
1093 entity: node_b,
1094 position: Vec2::new(100.0, 0.0),
1095 size: Vec2::new(50.0, 50.0),
1096 }, // Top-right
1097 FocusableArea {
1098 entity: node_c,
1099 position: Vec2::new(0.0, 100.0),
1100 size: Vec2::new(50.0, 50.0),
1101 }, // Bottom-left
1102 FocusableArea {
1103 entity: node_d,
1104 position: Vec2::new(100.0, 100.0),
1105 size: Vec2::new(50.0, 50.0),
1106 }, // Bottom-right
1107 ];
1108
1109 auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
1110
1111 // Test horizontal navigation
1112 assert_eq!(
1113 nav_map.get_neighbor(node_a, CompassOctant::East),
1114 Some(node_b)
1115 );
1116 assert_eq!(
1117 nav_map.get_neighbor(node_b, CompassOctant::West),
1118 Some(node_a)
1119 );
1120
1121 // Test vertical navigation
1122 assert_eq!(
1123 nav_map.get_neighbor(node_a, CompassOctant::South),
1124 Some(node_c)
1125 );
1126 assert_eq!(
1127 nav_map.get_neighbor(node_c, CompassOctant::North),
1128 Some(node_a)
1129 );
1130
1131 // Test diagonal navigation
1132 assert_eq!(
1133 nav_map.get_neighbor(node_a, CompassOctant::SouthEast),
1134 Some(node_d)
1135 );
1136 }
1137
1138 #[test]
1139 fn test_auto_generate_respects_manual_edges() {
1140 let mut nav_map = DirectionalNavigationMap::default();
1141 let config = AutoNavigationConfig::default();
1142
1143 let node_a = Entity::from_bits(1);
1144 let node_b = Entity::from_bits(2);
1145 let node_c = Entity::from_bits(3);
1146
1147 // Manually set an edge from A to C (skipping B)
1148 nav_map.add_edge(node_a, node_c, CompassOctant::East);
1149
1150 let nodes = vec![
1151 FocusableArea {
1152 entity: node_a,
1153 position: Vec2::new(0.0, 0.0),
1154 size: Vec2::new(50.0, 50.0),
1155 },
1156 FocusableArea {
1157 entity: node_b,
1158 position: Vec2::new(50.0, 0.0),
1159 size: Vec2::new(50.0, 50.0),
1160 }, // Closer
1161 FocusableArea {
1162 entity: node_c,
1163 position: Vec2::new(100.0, 0.0),
1164 size: Vec2::new(50.0, 50.0),
1165 },
1166 ];
1167
1168 auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
1169
1170 // The manual edge should be preserved, even though B is closer
1171 assert_eq!(
1172 nav_map.get_neighbor(node_a, CompassOctant::East),
1173 Some(node_c)
1174 );
1175 }
1176}