Skip to main content

bevy_input_focus/
directional_navigation.rs

1//! A manual navigation framework for moving between focusable elements based on directional input.
2//!
3//! Note: If using `bevy_ui`, this manual navigation framework is used to provide overrides
4//! for its automatic navigation framework based on the `AutoDirectionalNavigation` component.
5//! Most times, the automatic navigation framework alone should be sufficient.
6//! If not using `bevy_ui`, this manual navigation framework can still be used by itself.
7//!
8//! While virtual cursors are a common way to navigate UIs with a gamepad (or arrow keys!),
9//! they are generally both slow and frustrating to use.
10//! Instead, directional inputs should provide a direct way to snap between focusable elements.
11//!
12//! Like the rest of this crate, the [`InputFocus`] resource is manipulated to track
13//! the current focus.
14//!
15//! This module's [`DirectionalNavigationMap`] stores a directed graph of focusable entities.
16//! Each entity can have up to 8 neighbors, one for each [`CompassOctant`], balancing
17//! flexibility and required precision.
18//!
19//! Navigating between focusable entities (commonly UI nodes) is done by
20//! passing a [`CompassOctant`] into the [`navigate`](DirectionalNavigation::navigate) method
21//! from the [`DirectionalNavigation`] system parameter. Under the hood, the
22//! [`DirectionalNavigationMap`] is used to return the focusable entity in a direction
23//! for a given entity.
24//!
25//! # Setting up Directional Navigation
26//!
27//! ## Automatic Navigation (Recommended)
28//!
29//! The easiest way to set up navigation is to add the `AutoDirectionalNavigation` component
30//! to your UI entities. This component is available in the `bevy_ui` crate. If you choose to
31//! include automatic navigation, you should also use the `AutoDirectionalNavigator` system parameter
32//! in that crate instead of [`DirectionalNavigation`].
33//!
34//! ## Combining Automatic Navigation with Manual Overrides
35//!
36//! Following manual edges always take precedence, allowing you to use
37//! automatic navigation for most UI elements while overriding specific connections for
38//! special cases like wrapping menus or cross-layer navigation. If you need to override
39//! automatic navigation behavior, use the [`DirectionalNavigationMap`] to define
40//! overriding edges between UI entities.
41//!
42//! ## Manual Navigation Only
43//!
44//! Manually define your navigation using the [`DirectionalNavigationMap`], and use the
45//! [`DirectionalNavigation`] system parameter to navigate between components.
46//! You can define navigation connections using methods like
47//! [`add_edge`](DirectionalNavigationMap::add_edge) and
48//! [`add_looping_edges`](DirectionalNavigationMap::add_looping_edges).
49//!
50//! ## When to Use Manual Navigation or Manual Overrides
51//!
52//! While automatic navigation is recommended and satisfactory for most use cases,
53//! using manual navigation only or integrating manual overrides to automatic navigation provide:
54//!
55//! - **Precise control**: Define exact navigation flow, including non-obvious connections like looping edges
56//! - **Cross-layer navigation**: Connect elements across different UI layers or z-index levels
57//! - **Custom behavior**: Implement domain-specific navigation patterns (e.g., spreadsheet-style wrapping)
58
59use crate::{navigator::find_best_candidate, FocusCause, InputFocus};
60use bevy_app::prelude::*;
61use bevy_ecs::{
62    entity::{EntityHashMap, EntityHashSet},
63    prelude::*,
64    system::SystemParam,
65};
66use bevy_math::{CompassOctant, Vec2};
67use thiserror::Error;
68
69#[cfg(feature = "bevy_reflect")]
70use bevy_reflect::{prelude::*, Reflect};
71
72/// A plugin that sets up the directional navigation resources.
73#[derive(Default)]
74pub struct DirectionalNavigationPlugin;
75
76impl Plugin for DirectionalNavigationPlugin {
77    fn build(&self, app: &mut App) {
78        app.init_resource::<DirectionalNavigationMap>()
79            .init_resource::<AutoNavigationConfig>();
80    }
81}
82
83/// Configuration resource for automatic directional navigation and for generating manual
84/// navigation edges via [`auto_generate_navigation_edges`]
85///
86/// This resource controls how nodes should be automatically connected in each direction.
87#[derive(Resource, Debug, Clone, PartialEq)]
88#[cfg_attr(
89    feature = "bevy_reflect",
90    derive(Reflect),
91    reflect(Resource, Debug, PartialEq, Clone)
92)]
93pub struct AutoNavigationConfig {
94    /// Minimum overlap ratio (0.0-1.0) required along the perpendicular axis for cardinal directions.
95    ///
96    /// This parameter controls how much two UI elements must overlap in the perpendicular direction
97    /// to be considered reachable neighbors. It only applies to cardinal directions (`North`, `South`, `East`, `West`);
98    /// diagonal directions (`NorthEast`, `SouthEast`, etc.) ignore this requirement entirely.
99    ///
100    /// # Calculation
101    ///
102    /// The overlap factor is calculated as:
103    /// ```text
104    /// overlap_factor = actual_overlap / min(origin_size, candidate_size)
105    /// ```
106    ///
107    /// For East/West navigation, this measures vertical overlap:
108    /// - `actual_overlap` = overlapping height between the two elements
109    /// - Sizes are the heights of the origin and candidate
110    ///
111    /// For North/South navigation, this measures horizontal overlap:
112    /// - `actual_overlap` = overlapping width between the two elements
113    /// - Sizes are the widths of the origin and candidate
114    ///
115    /// # Examples
116    ///
117    /// - `0.0` (default): Any overlap is sufficient. Even if elements barely touch, they can be neighbors.
118    /// - `0.5`: Elements must overlap by at least 50% of the smaller element's size.
119    /// - `1.0`: Perfect alignment required. The smaller element must be completely within the bounds
120    ///   of the larger element along the perpendicular axis.
121    ///
122    /// # Use Cases
123    ///
124    /// - **Sparse/irregular layouts** (e.g., star constellations): Use `0.0` to allow navigation
125    ///   between elements that don't directly align.
126    /// - **Grid layouts**: Use `0.5` or higher to ensure navigation only connects elements in
127    ///   the same row or column.
128    /// - **Strict alignment**: Use `1.0` to require perfect alignment, though this may result
129    ///   in disconnected navigation graphs if elements aren't precisely aligned.
130    pub min_alignment_factor: f32,
131
132    /// Maximum search distance in logical pixels.
133    ///
134    /// Nodes beyond this distance won't be connected. `None` means unlimited.
135    /// The distance between two UI elements is calculated using their closest edges.
136    pub max_search_distance: Option<f32>,
137
138    /// Whether to prefer nodes that are more aligned with the exact direction.
139    ///
140    /// When `true`, nodes that are more directly in line with the requested direction
141    /// will be strongly preferred over nodes at an angle.
142    pub prefer_aligned: bool,
143}
144
145impl Default for AutoNavigationConfig {
146    fn default() -> Self {
147        Self {
148            min_alignment_factor: 0.0, // Any overlap is acceptable
149            max_search_distance: None, // No distance limit
150            prefer_aligned: true,      // Prefer well-aligned nodes
151        }
152    }
153}
154
155/// Represents what's near a focusable entity.
156#[derive(Default, Debug, Clone, PartialEq, Copy)]
157#[cfg_attr(
158    feature = "bevy_reflect",
159    derive(Reflect),
160    reflect(Default, Debug, PartialEq, Clone)
161)]
162pub enum NavNeighbor {
163    /// No neighbor explicitly set.
164    #[default]
165    Auto,
166    /// Do not find a neighbor.
167    Blocked,
168    /// The neighbor is known and set.
169    Set(Entity),
170}
171
172impl NavNeighbor {
173    /// Helper for getting the pointed-to entity, if any.
174    pub fn get(&self) -> Option<Entity> {
175        if let NavNeighbor::Set(n) = self {
176            Some(*n)
177        } else {
178            None
179        }
180    }
181}
182
183/// The up-to-eight neighbors of a focusable entity, one for each [`CompassOctant`].
184#[derive(Default, Debug, Clone, PartialEq)]
185#[cfg_attr(
186    feature = "bevy_reflect",
187    derive(Reflect),
188    reflect(Default, Debug, PartialEq, Clone)
189)]
190pub struct NavNeighbors {
191    /// The array of neighbors, one for each [`CompassOctant`].
192    /// The mapping between array elements and directions is determined by [`CompassOctant::to_index`].
193    ///
194    /// If no neighbor is set in a given direction, the value will be
195    /// [`NavNeighbor::Auto`].  If navigation should be explicitly blocked in a
196    /// given direction, the value will be [`NavNeighbor::Blocked`]. In most
197    /// cases, using [`NavNeighbors::set`], [`NavNeighbors::get`], and
198    /// [`NavNeighbors::block`] will be more ergonomic than directly accessing
199    /// this array.
200    pub neighbors: [NavNeighbor; 8],
201}
202
203impl NavNeighbors {
204    /// An empty set of neighbors.
205    pub const EMPTY: NavNeighbors = NavNeighbors {
206        neighbors: [NavNeighbor::Auto; 8],
207    };
208
209    /// Get the neighbor for a given [`CompassOctant`].
210    pub const fn get(&self, octant: CompassOctant) -> NavNeighbor {
211        self.neighbors[octant.to_index()]
212    }
213
214    /// Set the neighbor for a given [`CompassOctant`].
215    pub const fn set(&mut self, octant: CompassOctant, entity: Entity) {
216        self.neighbors[octant.to_index()] = NavNeighbor::Set(entity);
217    }
218
219    /// Prevent navigation to a given [`CompassOctant`].
220    ///
221    /// Note that navigation in this direction specifically will
222    /// be blocked. For example, blocking [`CompassOctant::North`]
223    /// will not affect the neighbor towards [`CompassOctant::NorthWest`].
224    pub const fn block(&mut self, octant: CompassOctant) {
225        self.neighbors[octant.to_index()] = NavNeighbor::Blocked;
226    }
227}
228
229/// A resource that stores the manually specified traversable graph of focusable entities.
230///
231/// Each entity can have up to 8 neighbors, one for each [`CompassOctant`].
232///
233/// To ensure that your graph is intuitive to navigate and generally works correctly, it should be:
234///
235/// - **Connected**: Every focusable entity should be reachable from every other focusable entity.
236/// - **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.
237/// - **Physical**: The direction of navigation should match the layout of the entities when possible,
238///   although looping around the edges of the screen is also acceptable.
239/// - **Not self-connected**: An entity should not be a neighbor of itself; use [`None`] instead.
240///
241/// This graph must be built and maintained manually, and the developer is responsible for ensuring that it meets the above criteria.
242/// Notably, if the developer adds or removes the navigability of an entity, the developer should update the map as necessary.
243///
244/// If the automatic navigation system in `bevy_ui` is being used, this resource can be used to specify
245/// manual navigation overrides. Any navigation edges specified in this map take precedence over automatic
246/// navigation. For example, if navigation on one side of the window should wrap around to
247/// the other side of the window, this navigation behavior can be specified using this map.
248#[derive(Resource, Debug, Default, Clone, PartialEq)]
249#[cfg_attr(
250    feature = "bevy_reflect",
251    derive(Reflect),
252    reflect(Resource, Debug, Default, PartialEq, Clone)
253)]
254pub struct DirectionalNavigationMap {
255    /// A directed graph of focusable entities.
256    ///
257    /// Pass in the current focus as a key, and get back a collection of up to 8 neighbors,
258    /// each keyed by a [`CompassOctant`].
259    pub neighbors: EntityHashMap<NavNeighbors>,
260}
261
262impl DirectionalNavigationMap {
263    /// Removes an entity from the navigation map, including all connections to and from it.
264    ///
265    /// Note that this is an O(n) operation, where n is the number of entities in the map,
266    /// as we must iterate over each entity to check for connections to the removed entity.
267    ///
268    /// If you are removing multiple entities, consider using [`remove_multiple`](Self::remove_multiple) instead.
269    pub fn remove(&mut self, entity: Entity) {
270        self.neighbors.remove(&entity);
271
272        for node in self.neighbors.values_mut() {
273            for neighbor in node.neighbors.iter_mut() {
274                if *neighbor == NavNeighbor::Set(entity) {
275                    *neighbor = NavNeighbor::Auto;
276                }
277            }
278        }
279    }
280
281    /// Removes a collection of entities from the navigation map.
282    ///
283    /// While this is still an O(n) operation, where n is the number of entities in the map,
284    /// it is more efficient than calling [`remove`](Self::remove) multiple times,
285    /// as we can check for connections to all removed entities in a single pass.
286    ///
287    /// An [`EntityHashSet`] must be provided as it is noticeably faster than the standard hasher or a [`Vec`](`alloc::vec::Vec`).
288    pub fn remove_multiple(&mut self, entities: EntityHashSet) {
289        for entity in &entities {
290            self.neighbors.remove(entity);
291        }
292
293        for node in self.neighbors.values_mut() {
294            for neighbor in node.neighbors.iter_mut() {
295                let NavNeighbor::Set(entity) = neighbor else {
296                    continue;
297                };
298                if entities.contains(entity) {
299                    *neighbor = NavNeighbor::Auto;
300                }
301            }
302        }
303    }
304
305    /// Completely clears the navigation map, removing all entities and connections.
306    pub fn clear(&mut self) {
307        self.neighbors.clear();
308    }
309
310    /// Adds an edge between two entities in the navigation map.
311    /// Any existing edge from A in the provided direction will be overwritten.
312    ///
313    /// The reverse edge will not be added, so navigation will only be possible in one direction.
314    /// If you want to add a symmetrical edge, use [`add_symmetrical_edge`](Self::add_symmetrical_edge) instead.
315    pub fn add_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
316        self.neighbors
317            .entry(a)
318            .or_insert(NavNeighbors::EMPTY)
319            .set(direction, b);
320    }
321
322    /// Adds an edge blocking automatic navigation from an entity in a direction.
323    /// Any existing edge from A in the provided direction will be overwritten.
324    ///
325    /// The reverse block will not be added, so navigation will still be possible from other entities
326    /// in the direction.
327    /// If you want to add a symmetrical block, use [`block_symmetrical_edge`](Self::block_symmetrical_edge) instead.
328    ///
329    /// Note that blocking a primary cardinal direction will not block intermediates.
330    /// In other words, blocking `North` will still allow navigation towards `NorthEast`.
331    pub fn block_edge(&mut self, a: Entity, direction: CompassOctant) {
332        self.neighbors
333            .entry(a)
334            .or_insert(NavNeighbors::EMPTY)
335            .block(direction);
336    }
337
338    /// Adds a symmetrical edge between two entities in the navigation map.
339    /// The A -> B path will use the provided direction, while B -> A will use the [`CompassOctant::opposite`] variant.
340    ///
341    /// Any existing connections between the two entities will be overwritten.
342    pub fn add_symmetrical_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
343        self.add_edge(a, b, direction);
344        self.add_edge(b, a, direction.opposite());
345    }
346
347    /// Adds a symmetrical blocking edge between two entities in the navigation map.
348    /// The blocked A -> B path will use the provided direction, while B -> A will use the [`CompassOctant::opposite`] variant.
349    ///
350    /// Any existing connections between the two entities will be overwritten.
351    pub fn block_symmetrical_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
352        self.block_edge(a, direction);
353        self.block_edge(b, direction.opposite());
354    }
355
356    /// Add symmetrical edges between each consecutive pair of entities in the provided slice.
357    ///
358    /// Unlike [`add_looping_edges`](Self::add_looping_edges), this method does not loop back to the first entity.
359    pub fn add_edges(&mut self, entities: &[Entity], direction: CompassOctant) {
360        for &[a, b] in entities.array_windows() {
361            self.add_symmetrical_edge(a, b, direction);
362        }
363    }
364
365    /// Add symmetrical edges between each consecutive pair of entities in the provided slice, looping back to the first entity at the end.
366    ///
367    /// This is useful for creating a circular navigation path between a set of entities, such as a menu.
368    pub fn add_looping_edges(&mut self, entities: &[Entity], direction: CompassOctant) {
369        self.add_edges(entities, direction);
370        if let Some((first_entity, rest)) = entities.split_first()
371            && let Some(last_entity) = rest.last()
372        {
373            self.add_symmetrical_edge(*last_entity, *first_entity, direction);
374        }
375    }
376
377    /// Gets the entity in a given direction from the current focus, if any.
378    pub fn get_neighbor(&self, focus: Entity, octant: CompassOctant) -> NavNeighbor {
379        self.neighbors
380            .get(&focus)
381            .map(|neighbors| neighbors.get(octant))
382            .unwrap_or(NavNeighbor::Auto)
383    }
384
385    /// Looks up the neighbors of a given entity.
386    ///
387    /// If the entity is not in the map, [`None`] will be returned.
388    /// Note that the set of neighbors may be empty!
389    pub fn get_neighbors(&self, entity: Entity) -> Option<&NavNeighbors> {
390        self.neighbors.get(&entity)
391    }
392}
393
394/// A system parameter for navigating between focusable entities in a directional way.
395#[derive(SystemParam, Debug)]
396pub struct DirectionalNavigation<'w> {
397    /// The currently focused entity.
398    pub focus: ResMut<'w, InputFocus>,
399    /// The directional navigation map containing manually defined connections between entities.
400    pub map: Res<'w, DirectionalNavigationMap>,
401}
402
403impl<'w> DirectionalNavigation<'w> {
404    /// Navigates to the neighbor in a given direction from the current focus, if any.
405    ///
406    /// Returns the new focus if successful.
407    /// Returns an error if there is no focus set or if there is no neighbor in the requested direction.
408    ///
409    /// If the result was `Ok`, the [`InputFocus`] resource is updated to the new focus as part of this method call.
410    pub fn navigate(
411        &mut self,
412        direction: CompassOctant,
413    ) -> Result<Entity, DirectionalNavigationError> {
414        if let Some(current_focus) = self.focus.get() {
415            // Respect manual edges first
416            match self.map.get_neighbor(current_focus, direction) {
417                NavNeighbor::Auto => Err(DirectionalNavigationError::NoNeighborInDirection {
418                    current_focus,
419                    direction,
420                }),
421                NavNeighbor::Blocked => Err(DirectionalNavigationError::BlockedNavigation {
422                    current_focus,
423                    direction,
424                }),
425                NavNeighbor::Set(new_focus) => {
426                    self.focus.set(new_focus, FocusCause::Navigated);
427                    Ok(new_focus)
428                }
429            }
430        } else {
431            Err(DirectionalNavigationError::NoFocus)
432        }
433    }
434}
435
436/// An error that can occur when navigating between focusable entities using [directional navigation](crate::directional_navigation).
437#[derive(Debug, PartialEq, Clone, Error)]
438pub enum DirectionalNavigationError {
439    /// No focusable entity is currently set.
440    #[error("No focusable entity is currently set.")]
441    NoFocus,
442    /// No neighbor in the requested direction.
443    #[error("No neighbor from {current_focus} in the {direction:?} direction.")]
444    NoNeighborInDirection {
445        /// The entity that was the focus when the error occurred.
446        current_focus: Entity,
447        /// The direction in which the navigation was attempted.
448        direction: CompassOctant,
449    },
450    /// Navigation explicitly blocked in the requested direction.
451    #[error("Navigation explicitly blocked from {current_focus} in the {direction:?} direction.")]
452    BlockedNavigation {
453        /// The entity that was the focus when the error occurred.
454        current_focus: Entity,
455        /// The direction in which the navigation was attempted.
456        direction: CompassOctant,
457    },
458}
459
460/// A focusable area with position and size information.
461///
462/// This struct represents a UI element used during directional navigation,
463/// containing its entity ID, center position, and size for spatial navigation calculations.
464///
465/// The term "focusable area" avoids confusion with UI `Node` components in `bevy_ui`.
466#[derive(Debug, Clone, Copy, PartialEq)]
467#[cfg_attr(
468    feature = "bevy_reflect",
469    derive(Reflect),
470    reflect(Debug, PartialEq, Clone)
471)]
472pub struct FocusableArea {
473    /// The entity identifier for this focusable area.
474    pub entity: Entity,
475    /// The center position in global coordinates.
476    pub position: Vec2,
477    /// The size (width, height) of the area.
478    pub size: Vec2,
479}
480
481/// Trait for extracting position and size from navigable UI components.
482///
483/// This allows the auto-navigation system to work with different UI implementations
484/// as long as they can provide position and size information.
485pub trait Navigable {
486    /// Returns the center position and size in global coordinates.
487    fn get_bounds(&self) -> (Vec2, Vec2);
488}
489
490/// Automatically generates directional navigation edges for a collection of nodes.
491///
492/// This function takes a slice of navigation nodes with their positions and sizes, and populates
493/// the navigation map with edges to the nearest neighbor in each compass direction.
494/// Manual edges already in the map are preserved and not overwritten.
495///
496/// # Arguments
497///
498/// * `nav_map` - The navigation map to populate
499/// * `nodes` - A slice of [`FocusableArea`] structs containing entity, position, and size data
500/// * `config` - Configuration for the auto-generation algorithm
501///
502/// # Example
503///
504/// ```rust
505/// # use bevy_input_focus::{directional_navigation::*, FocusCause};
506/// # use bevy_ecs::entity::Entity;
507/// # use bevy_math::Vec2;
508/// let mut nav_map = DirectionalNavigationMap::default();
509/// let config = AutoNavigationConfig::default();
510///
511/// let nodes = vec![
512///     FocusableArea { entity: Entity::PLACEHOLDER, position: Vec2::new(100.0, 100.0), size: Vec2::new(50.0, 50.0) },
513///     FocusableArea { entity: Entity::PLACEHOLDER, position: Vec2::new(200.0, 100.0), size: Vec2::new(50.0, 50.0) },
514/// ];
515///
516/// auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
517/// ```
518pub fn auto_generate_navigation_edges(
519    nav_map: &mut DirectionalNavigationMap,
520    nodes: &[FocusableArea],
521    config: &AutoNavigationConfig,
522) {
523    // For each node, find best neighbor in each direction
524    for origin in nodes {
525        for octant in [
526            CompassOctant::North,
527            CompassOctant::NorthEast,
528            CompassOctant::East,
529            CompassOctant::SouthEast,
530            CompassOctant::South,
531            CompassOctant::SouthWest,
532            CompassOctant::West,
533            CompassOctant::NorthWest,
534        ] {
535            // Skip if manual edge already exists (check inline to avoid borrow issues)
536            if nav_map
537                .get_neighbors(origin.entity)
538                .filter(|neighbors| {
539                    matches!(
540                        neighbors.get(octant),
541                        NavNeighbor::Blocked | NavNeighbor::Set(_)
542                    )
543                })
544                .is_some()
545            {
546                continue; // Respect manual override
547            }
548
549            // Find best candidate in this direction
550            let best_candidate = find_best_candidate(origin, octant, nodes, config);
551
552            // Add edge if we found a valid candidate
553            if let Some(neighbor) = best_candidate {
554                nav_map.add_edge(origin.entity, neighbor, octant);
555            }
556        }
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use alloc::vec;
563    use bevy_ecs::system::RunSystemOnce;
564
565    use super::*;
566
567    #[test]
568    fn setting_and_getting_nav_neighbors() {
569        let mut neighbors = NavNeighbors::EMPTY;
570        assert_eq!(neighbors.get(CompassOctant::SouthEast), NavNeighbor::Auto);
571
572        let entity = World::default().spawn_empty().id();
573
574        neighbors.set(CompassOctant::SouthEast, entity);
575
576        for i in 0..8 {
577            if i == CompassOctant::SouthEast.to_index() {
578                assert_eq!(
579                    neighbors.get(CompassOctant::SouthEast),
580                    NavNeighbor::Set(entity)
581                );
582            } else {
583                assert_eq!(
584                    neighbors.get(CompassOctant::from_index(i).unwrap()),
585                    NavNeighbor::Auto
586                );
587            }
588        }
589    }
590
591    #[test]
592    fn simple_set_and_get_navmap() {
593        let mut world = World::new();
594        let a = world.spawn_empty().id();
595        let b = world.spawn_empty().id();
596
597        let mut map = DirectionalNavigationMap::default();
598        map.add_edge(a, b, CompassOctant::SouthEast);
599
600        assert_eq!(
601            map.get_neighbor(a, CompassOctant::SouthEast),
602            NavNeighbor::Set(b)
603        );
604        assert_eq!(
605            map.get_neighbor(b, CompassOctant::SouthEast.opposite()),
606            NavNeighbor::Auto
607        );
608    }
609
610    #[test]
611    fn symmetrical_edges() {
612        let mut world = World::new();
613        let a = world.spawn_empty().id();
614        let b = world.spawn_empty().id();
615
616        let mut map = DirectionalNavigationMap::default();
617        map.add_symmetrical_edge(a, b, CompassOctant::North);
618
619        assert_eq!(
620            map.get_neighbor(a, CompassOctant::North),
621            NavNeighbor::Set(b)
622        );
623        assert_eq!(
624            map.get_neighbor(b, CompassOctant::South),
625            NavNeighbor::Set(a)
626        );
627    }
628
629    #[test]
630    fn remove_nodes() {
631        let mut world = World::new();
632        let a = world.spawn_empty().id();
633        let b = world.spawn_empty().id();
634
635        let mut map = DirectionalNavigationMap::default();
636        map.add_edge(a, b, CompassOctant::North);
637        map.add_edge(b, a, CompassOctant::South);
638
639        assert_eq!(
640            map.get_neighbor(a, CompassOctant::North),
641            NavNeighbor::Set(b)
642        );
643        assert_eq!(
644            map.get_neighbor(b, CompassOctant::South),
645            NavNeighbor::Set(a)
646        );
647
648        map.remove(b);
649
650        assert_eq!(map.get_neighbor(a, CompassOctant::North), NavNeighbor::Auto);
651        assert_eq!(map.get_neighbor(b, CompassOctant::South), NavNeighbor::Auto);
652    }
653
654    #[test]
655    fn remove_multiple_nodes() {
656        let mut world = World::new();
657        let a = world.spawn_empty().id();
658        let b = world.spawn_empty().id();
659        let c = world.spawn_empty().id();
660
661        let mut map = DirectionalNavigationMap::default();
662        map.add_edge(a, b, CompassOctant::North);
663        map.add_edge(b, a, CompassOctant::South);
664        map.add_edge(b, c, CompassOctant::East);
665        map.add_edge(c, b, CompassOctant::West);
666
667        let mut to_remove = EntityHashSet::default();
668        to_remove.insert(b);
669        to_remove.insert(c);
670
671        map.remove_multiple(to_remove);
672
673        assert_eq!(map.get_neighbor(a, CompassOctant::North), NavNeighbor::Auto);
674        assert_eq!(map.get_neighbor(b, CompassOctant::South), NavNeighbor::Auto);
675        assert_eq!(map.get_neighbor(b, CompassOctant::East), NavNeighbor::Auto);
676        assert_eq!(map.get_neighbor(c, CompassOctant::West), NavNeighbor::Auto);
677    }
678
679    #[test]
680    fn edges() {
681        let mut world = World::new();
682        let a = world.spawn_empty().id();
683        let b = world.spawn_empty().id();
684        let c = world.spawn_empty().id();
685
686        let mut map = DirectionalNavigationMap::default();
687        map.add_edges(&[a, b, c], CompassOctant::East);
688
689        assert_eq!(
690            map.get_neighbor(a, CompassOctant::East),
691            NavNeighbor::Set(b)
692        );
693        assert_eq!(
694            map.get_neighbor(b, CompassOctant::East),
695            NavNeighbor::Set(c)
696        );
697        assert_eq!(map.get_neighbor(c, CompassOctant::East), NavNeighbor::Auto);
698
699        assert_eq!(map.get_neighbor(a, CompassOctant::West), NavNeighbor::Auto);
700        assert_eq!(
701            map.get_neighbor(b, CompassOctant::West),
702            NavNeighbor::Set(a)
703        );
704        assert_eq!(
705            map.get_neighbor(c, CompassOctant::West),
706            NavNeighbor::Set(b)
707        );
708    }
709
710    #[test]
711    fn looping_edges() {
712        let mut world = World::new();
713        let a = world.spawn_empty().id();
714        let b = world.spawn_empty().id();
715        let c = world.spawn_empty().id();
716
717        let mut map = DirectionalNavigationMap::default();
718        map.add_looping_edges(&[a, b, c], CompassOctant::East);
719
720        assert_eq!(
721            map.get_neighbor(a, CompassOctant::East),
722            NavNeighbor::Set(b)
723        );
724        assert_eq!(
725            map.get_neighbor(b, CompassOctant::East),
726            NavNeighbor::Set(c)
727        );
728        assert_eq!(
729            map.get_neighbor(c, CompassOctant::East),
730            NavNeighbor::Set(a)
731        );
732
733        assert_eq!(
734            map.get_neighbor(a, CompassOctant::West),
735            NavNeighbor::Set(c)
736        );
737        assert_eq!(
738            map.get_neighbor(b, CompassOctant::West),
739            NavNeighbor::Set(a)
740        );
741        assert_eq!(
742            map.get_neighbor(c, CompassOctant::West),
743            NavNeighbor::Set(b)
744        );
745    }
746
747    #[test]
748    fn manual_nav_with_system_param() {
749        let mut world = World::new();
750        let a = world.spawn_empty().id();
751        let b = world.spawn_empty().id();
752        let c = world.spawn_empty().id();
753
754        let mut map = DirectionalNavigationMap::default();
755        map.add_looping_edges(&[a, b, c], CompassOctant::East);
756
757        world.insert_resource(map);
758
759        let mut focus = InputFocus::default();
760        focus.set(a, FocusCause::Navigated);
761        world.insert_resource(focus);
762
763        let config = AutoNavigationConfig::default();
764        world.insert_resource(config);
765
766        assert_eq!(world.resource::<InputFocus>().get(), Some(a));
767
768        fn navigate_east(mut nav: DirectionalNavigation) {
769            nav.navigate(CompassOctant::East).unwrap();
770        }
771
772        world.run_system_once(navigate_east).unwrap();
773        assert_eq!(world.resource::<InputFocus>().get(), Some(b));
774
775        world.run_system_once(navigate_east).unwrap();
776        assert_eq!(world.resource::<InputFocus>().get(), Some(c));
777
778        world.run_system_once(navigate_east).unwrap();
779        assert_eq!(world.resource::<InputFocus>().get(), Some(a));
780    }
781
782    #[test]
783    fn test_auto_generate_navigation_edges() {
784        let mut nav_map = DirectionalNavigationMap::default();
785        let config = AutoNavigationConfig::default();
786
787        // Create a 2x2 grid of nodes (using UI coordinates: smaller Y = higher on screen)
788        let node_a = Entity::from_bits(1); // Top-left
789        let node_b = Entity::from_bits(2); // Top-right
790        let node_c = Entity::from_bits(3); // Bottom-left
791        let node_d = Entity::from_bits(4); // Bottom-right
792
793        let nodes = vec![
794            FocusableArea {
795                entity: node_a,
796                position: Vec2::new(0.0, 0.0),
797                size: Vec2::new(50.0, 50.0),
798            }, // Top-left
799            FocusableArea {
800                entity: node_b,
801                position: Vec2::new(100.0, 0.0),
802                size: Vec2::new(50.0, 50.0),
803            }, // Top-right
804            FocusableArea {
805                entity: node_c,
806                position: Vec2::new(0.0, 100.0),
807                size: Vec2::new(50.0, 50.0),
808            }, // Bottom-left
809            FocusableArea {
810                entity: node_d,
811                position: Vec2::new(100.0, 100.0),
812                size: Vec2::new(50.0, 50.0),
813            }, // Bottom-right
814        ];
815
816        auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
817
818        // Test horizontal navigation
819        assert_eq!(
820            nav_map.get_neighbor(node_a, CompassOctant::East),
821            NavNeighbor::Set(node_b)
822        );
823        assert_eq!(
824            nav_map.get_neighbor(node_b, CompassOctant::West),
825            NavNeighbor::Set(node_a)
826        );
827
828        // Test vertical navigation
829        assert_eq!(
830            nav_map.get_neighbor(node_a, CompassOctant::South),
831            NavNeighbor::Set(node_c)
832        );
833        assert_eq!(
834            nav_map.get_neighbor(node_c, CompassOctant::North),
835            NavNeighbor::Set(node_a)
836        );
837
838        // Test diagonal navigation
839        assert_eq!(
840            nav_map.get_neighbor(node_a, CompassOctant::SouthEast),
841            NavNeighbor::Set(node_d)
842        );
843    }
844
845    #[test]
846    fn test_auto_generate_respects_manual_edges() {
847        let mut nav_map = DirectionalNavigationMap::default();
848        let config = AutoNavigationConfig::default();
849
850        let node_a = Entity::from_bits(1);
851        let node_b = Entity::from_bits(2);
852        let node_c = Entity::from_bits(3);
853
854        // Manually set an edge from A to C (skipping B)
855        nav_map.add_edge(node_a, node_c, CompassOctant::East);
856
857        let nodes = vec![
858            FocusableArea {
859                entity: node_a,
860                position: Vec2::new(0.0, 0.0),
861                size: Vec2::new(50.0, 50.0),
862            },
863            FocusableArea {
864                entity: node_b,
865                position: Vec2::new(50.0, 0.0),
866                size: Vec2::new(50.0, 50.0),
867            }, // Closer
868            FocusableArea {
869                entity: node_c,
870                position: Vec2::new(100.0, 0.0),
871                size: Vec2::new(50.0, 50.0),
872            },
873        ];
874
875        auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
876
877        // The manual edge should be preserved, even though B is closer
878        assert_eq!(
879            nav_map.get_neighbor(node_a, CompassOctant::East),
880            NavNeighbor::Set(node_c)
881        );
882    }
883
884    #[test]
885    fn test_edge_distance_vs_center_distance() {
886        let mut nav_map = DirectionalNavigationMap::default();
887        let config = AutoNavigationConfig::default();
888
889        let left = Entity::from_bits(1);
890        let wide_top = Entity::from_bits(2);
891        let bottom = Entity::from_bits(3);
892
893        let left_node = FocusableArea {
894            entity: left,
895            position: Vec2::new(100.0, 200.0),
896            size: Vec2::new(100.0, 100.0),
897        };
898
899        let wide_top_node = FocusableArea {
900            entity: wide_top,
901            position: Vec2::new(350.0, 150.0),
902            size: Vec2::new(300.0, 80.0),
903        };
904
905        let bottom_node = FocusableArea {
906            entity: bottom,
907            position: Vec2::new(270.0, 300.0),
908            size: Vec2::new(100.0, 80.0),
909        };
910
911        let nodes = vec![left_node, wide_top_node, bottom_node];
912
913        auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
914
915        assert_eq!(
916            nav_map.get_neighbor(left, CompassOctant::East),
917            NavNeighbor::Set(wide_top),
918            "Should navigate to wide_top not bottom, even though bottom's center is closer."
919        );
920    }
921
922    #[test]
923    fn test_respects_set_blocks() {
924        let mut nav_map = DirectionalNavigationMap::default();
925        let config = AutoNavigationConfig::default();
926
927        let node_a = Entity::from_bits(1);
928        let node_b = Entity::from_bits(2);
929        let node_c = Entity::from_bits(3);
930
931        // Manually set a block from A to B
932        // A should NOT be able to nav East to B
933        // but SHOULD be able to nav South to C
934        nav_map.block_edge(node_a, CompassOctant::East);
935
936        let nodes = vec![
937            FocusableArea {
938                entity: node_a,
939                position: Vec2::new(0.0, 0.0),
940                size: Vec2::new(50.0, 50.0),
941            },
942            FocusableArea {
943                entity: node_b,
944                position: Vec2::new(50.0, 0.0),
945                size: Vec2::new(50.0, 50.0),
946            },
947            FocusableArea {
948                entity: node_c,
949                position: Vec2::new(0.0, 50.0),
950                size: Vec2::new(50.0, 50.0),
951            },
952        ];
953
954        auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
955
956        // The manual edge should be preserved, even though B is closer
957        assert_eq!(
958            nav_map.get_neighbor(node_a, CompassOctant::East),
959            NavNeighbor::Blocked
960        );
961        // But automatic edges should still be populated
962        assert_eq!(
963            nav_map.get_neighbor(node_a, CompassOctant::South),
964            NavNeighbor::Set(node_c)
965        );
966    }
967}