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}