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 neighbors.set(CompassOctant::SouthEast, Entity::PLACEHOLDER);
573
574 for i in 0..8 {
575 if i == CompassOctant::SouthEast.to_index() {
576 assert_eq!(
577 neighbors.get(CompassOctant::SouthEast),
578 NavNeighbor::Set(Entity::PLACEHOLDER)
579 );
580 } else {
581 assert_eq!(
582 neighbors.get(CompassOctant::from_index(i).unwrap()),
583 NavNeighbor::Auto
584 );
585 }
586 }
587 }
588
589 #[test]
590 fn simple_set_and_get_navmap() {
591 let mut world = World::new();
592 let a = world.spawn_empty().id();
593 let b = world.spawn_empty().id();
594
595 let mut map = DirectionalNavigationMap::default();
596 map.add_edge(a, b, CompassOctant::SouthEast);
597
598 assert_eq!(
599 map.get_neighbor(a, CompassOctant::SouthEast),
600 NavNeighbor::Set(b)
601 );
602 assert_eq!(
603 map.get_neighbor(b, CompassOctant::SouthEast.opposite()),
604 NavNeighbor::Auto
605 );
606 }
607
608 #[test]
609 fn symmetrical_edges() {
610 let mut world = World::new();
611 let a = world.spawn_empty().id();
612 let b = world.spawn_empty().id();
613
614 let mut map = DirectionalNavigationMap::default();
615 map.add_symmetrical_edge(a, b, CompassOctant::North);
616
617 assert_eq!(
618 map.get_neighbor(a, CompassOctant::North),
619 NavNeighbor::Set(b)
620 );
621 assert_eq!(
622 map.get_neighbor(b, CompassOctant::South),
623 NavNeighbor::Set(a)
624 );
625 }
626
627 #[test]
628 fn remove_nodes() {
629 let mut world = World::new();
630 let a = world.spawn_empty().id();
631 let b = world.spawn_empty().id();
632
633 let mut map = DirectionalNavigationMap::default();
634 map.add_edge(a, b, CompassOctant::North);
635 map.add_edge(b, a, CompassOctant::South);
636
637 assert_eq!(
638 map.get_neighbor(a, CompassOctant::North),
639 NavNeighbor::Set(b)
640 );
641 assert_eq!(
642 map.get_neighbor(b, CompassOctant::South),
643 NavNeighbor::Set(a)
644 );
645
646 map.remove(b);
647
648 assert_eq!(map.get_neighbor(a, CompassOctant::North), NavNeighbor::Auto);
649 assert_eq!(map.get_neighbor(b, CompassOctant::South), NavNeighbor::Auto);
650 }
651
652 #[test]
653 fn remove_multiple_nodes() {
654 let mut world = World::new();
655 let a = world.spawn_empty().id();
656 let b = world.spawn_empty().id();
657 let c = world.spawn_empty().id();
658
659 let mut map = DirectionalNavigationMap::default();
660 map.add_edge(a, b, CompassOctant::North);
661 map.add_edge(b, a, CompassOctant::South);
662 map.add_edge(b, c, CompassOctant::East);
663 map.add_edge(c, b, CompassOctant::West);
664
665 let mut to_remove = EntityHashSet::default();
666 to_remove.insert(b);
667 to_remove.insert(c);
668
669 map.remove_multiple(to_remove);
670
671 assert_eq!(map.get_neighbor(a, CompassOctant::North), NavNeighbor::Auto);
672 assert_eq!(map.get_neighbor(b, CompassOctant::South), NavNeighbor::Auto);
673 assert_eq!(map.get_neighbor(b, CompassOctant::East), NavNeighbor::Auto);
674 assert_eq!(map.get_neighbor(c, CompassOctant::West), NavNeighbor::Auto);
675 }
676
677 #[test]
678 fn edges() {
679 let mut world = World::new();
680 let a = world.spawn_empty().id();
681 let b = world.spawn_empty().id();
682 let c = world.spawn_empty().id();
683
684 let mut map = DirectionalNavigationMap::default();
685 map.add_edges(&[a, b, c], CompassOctant::East);
686
687 assert_eq!(
688 map.get_neighbor(a, CompassOctant::East),
689 NavNeighbor::Set(b)
690 );
691 assert_eq!(
692 map.get_neighbor(b, CompassOctant::East),
693 NavNeighbor::Set(c)
694 );
695 assert_eq!(map.get_neighbor(c, CompassOctant::East), NavNeighbor::Auto);
696
697 assert_eq!(map.get_neighbor(a, CompassOctant::West), NavNeighbor::Auto);
698 assert_eq!(
699 map.get_neighbor(b, CompassOctant::West),
700 NavNeighbor::Set(a)
701 );
702 assert_eq!(
703 map.get_neighbor(c, CompassOctant::West),
704 NavNeighbor::Set(b)
705 );
706 }
707
708 #[test]
709 fn looping_edges() {
710 let mut world = World::new();
711 let a = world.spawn_empty().id();
712 let b = world.spawn_empty().id();
713 let c = world.spawn_empty().id();
714
715 let mut map = DirectionalNavigationMap::default();
716 map.add_looping_edges(&[a, b, c], CompassOctant::East);
717
718 assert_eq!(
719 map.get_neighbor(a, CompassOctant::East),
720 NavNeighbor::Set(b)
721 );
722 assert_eq!(
723 map.get_neighbor(b, CompassOctant::East),
724 NavNeighbor::Set(c)
725 );
726 assert_eq!(
727 map.get_neighbor(c, CompassOctant::East),
728 NavNeighbor::Set(a)
729 );
730
731 assert_eq!(
732 map.get_neighbor(a, CompassOctant::West),
733 NavNeighbor::Set(c)
734 );
735 assert_eq!(
736 map.get_neighbor(b, CompassOctant::West),
737 NavNeighbor::Set(a)
738 );
739 assert_eq!(
740 map.get_neighbor(c, CompassOctant::West),
741 NavNeighbor::Set(b)
742 );
743 }
744
745 #[test]
746 fn manual_nav_with_system_param() {
747 let mut world = World::new();
748 let a = world.spawn_empty().id();
749 let b = world.spawn_empty().id();
750 let c = world.spawn_empty().id();
751
752 let mut map = DirectionalNavigationMap::default();
753 map.add_looping_edges(&[a, b, c], CompassOctant::East);
754
755 world.insert_resource(map);
756
757 let mut focus = InputFocus::default();
758 focus.set(a, FocusCause::Navigated);
759 world.insert_resource(focus);
760
761 let config = AutoNavigationConfig::default();
762 world.insert_resource(config);
763
764 assert_eq!(world.resource::<InputFocus>().get(), Some(a));
765
766 fn navigate_east(mut nav: DirectionalNavigation) {
767 nav.navigate(CompassOctant::East).unwrap();
768 }
769
770 world.run_system_once(navigate_east).unwrap();
771 assert_eq!(world.resource::<InputFocus>().get(), Some(b));
772
773 world.run_system_once(navigate_east).unwrap();
774 assert_eq!(world.resource::<InputFocus>().get(), Some(c));
775
776 world.run_system_once(navigate_east).unwrap();
777 assert_eq!(world.resource::<InputFocus>().get(), Some(a));
778 }
779
780 #[test]
781 fn test_auto_generate_navigation_edges() {
782 let mut nav_map = DirectionalNavigationMap::default();
783 let config = AutoNavigationConfig::default();
784
785 // Create a 2x2 grid of nodes (using UI coordinates: smaller Y = higher on screen)
786 let node_a = Entity::from_bits(1); // Top-left
787 let node_b = Entity::from_bits(2); // Top-right
788 let node_c = Entity::from_bits(3); // Bottom-left
789 let node_d = Entity::from_bits(4); // Bottom-right
790
791 let nodes = vec![
792 FocusableArea {
793 entity: node_a,
794 position: Vec2::new(0.0, 0.0),
795 size: Vec2::new(50.0, 50.0),
796 }, // Top-left
797 FocusableArea {
798 entity: node_b,
799 position: Vec2::new(100.0, 0.0),
800 size: Vec2::new(50.0, 50.0),
801 }, // Top-right
802 FocusableArea {
803 entity: node_c,
804 position: Vec2::new(0.0, 100.0),
805 size: Vec2::new(50.0, 50.0),
806 }, // Bottom-left
807 FocusableArea {
808 entity: node_d,
809 position: Vec2::new(100.0, 100.0),
810 size: Vec2::new(50.0, 50.0),
811 }, // Bottom-right
812 ];
813
814 auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
815
816 // Test horizontal navigation
817 assert_eq!(
818 nav_map.get_neighbor(node_a, CompassOctant::East),
819 NavNeighbor::Set(node_b)
820 );
821 assert_eq!(
822 nav_map.get_neighbor(node_b, CompassOctant::West),
823 NavNeighbor::Set(node_a)
824 );
825
826 // Test vertical navigation
827 assert_eq!(
828 nav_map.get_neighbor(node_a, CompassOctant::South),
829 NavNeighbor::Set(node_c)
830 );
831 assert_eq!(
832 nav_map.get_neighbor(node_c, CompassOctant::North),
833 NavNeighbor::Set(node_a)
834 );
835
836 // Test diagonal navigation
837 assert_eq!(
838 nav_map.get_neighbor(node_a, CompassOctant::SouthEast),
839 NavNeighbor::Set(node_d)
840 );
841 }
842
843 #[test]
844 fn test_auto_generate_respects_manual_edges() {
845 let mut nav_map = DirectionalNavigationMap::default();
846 let config = AutoNavigationConfig::default();
847
848 let node_a = Entity::from_bits(1);
849 let node_b = Entity::from_bits(2);
850 let node_c = Entity::from_bits(3);
851
852 // Manually set an edge from A to C (skipping B)
853 nav_map.add_edge(node_a, node_c, CompassOctant::East);
854
855 let nodes = vec![
856 FocusableArea {
857 entity: node_a,
858 position: Vec2::new(0.0, 0.0),
859 size: Vec2::new(50.0, 50.0),
860 },
861 FocusableArea {
862 entity: node_b,
863 position: Vec2::new(50.0, 0.0),
864 size: Vec2::new(50.0, 50.0),
865 }, // Closer
866 FocusableArea {
867 entity: node_c,
868 position: Vec2::new(100.0, 0.0),
869 size: Vec2::new(50.0, 50.0),
870 },
871 ];
872
873 auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
874
875 // The manual edge should be preserved, even though B is closer
876 assert_eq!(
877 nav_map.get_neighbor(node_a, CompassOctant::East),
878 NavNeighbor::Set(node_c)
879 );
880 }
881
882 #[test]
883 fn test_edge_distance_vs_center_distance() {
884 let mut nav_map = DirectionalNavigationMap::default();
885 let config = AutoNavigationConfig::default();
886
887 let left = Entity::from_bits(1);
888 let wide_top = Entity::from_bits(2);
889 let bottom = Entity::from_bits(3);
890
891 let left_node = FocusableArea {
892 entity: left,
893 position: Vec2::new(100.0, 200.0),
894 size: Vec2::new(100.0, 100.0),
895 };
896
897 let wide_top_node = FocusableArea {
898 entity: wide_top,
899 position: Vec2::new(350.0, 150.0),
900 size: Vec2::new(300.0, 80.0),
901 };
902
903 let bottom_node = FocusableArea {
904 entity: bottom,
905 position: Vec2::new(270.0, 300.0),
906 size: Vec2::new(100.0, 80.0),
907 };
908
909 let nodes = vec![left_node, wide_top_node, bottom_node];
910
911 auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
912
913 assert_eq!(
914 nav_map.get_neighbor(left, CompassOctant::East),
915 NavNeighbor::Set(wide_top),
916 "Should navigate to wide_top not bottom, even though bottom's center is closer."
917 );
918 }
919
920 #[test]
921 fn test_respects_set_blocks() {
922 let mut nav_map = DirectionalNavigationMap::default();
923 let config = AutoNavigationConfig::default();
924
925 let node_a = Entity::from_bits(1);
926 let node_b = Entity::from_bits(2);
927 let node_c = Entity::from_bits(3);
928
929 // Manually set a block from A to B
930 // A should NOT be able to nav East to B
931 // but SHOULD be able to nav South to C
932 nav_map.block_edge(node_a, CompassOctant::East);
933
934 let nodes = vec![
935 FocusableArea {
936 entity: node_a,
937 position: Vec2::new(0.0, 0.0),
938 size: Vec2::new(50.0, 50.0),
939 },
940 FocusableArea {
941 entity: node_b,
942 position: Vec2::new(50.0, 0.0),
943 size: Vec2::new(50.0, 50.0),
944 },
945 FocusableArea {
946 entity: node_c,
947 position: Vec2::new(0.0, 50.0),
948 size: Vec2::new(50.0, 50.0),
949 },
950 ];
951
952 auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
953
954 // The manual edge should be preserved, even though B is closer
955 assert_eq!(
956 nav_map.get_neighbor(node_a, CompassOctant::East),
957 NavNeighbor::Blocked
958 );
959 // But automatic edges should still be populated
960 assert_eq!(
961 nav_map.get_neighbor(node_a, CompassOctant::South),
962 NavNeighbor::Set(node_c)
963 );
964 }
965}