From 68e9bd77441475a423e36b4d3aadd27e7f48da8f Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Thu, 1 Jan 2026 01:18:51 -0500 Subject: [PATCH 01/15] refactor: separates bevy_ui specific items to auto_directional_navigation.rs --- .../src/auto_directional_navigation.rs | 209 ++++++ .../src/directional_navigation.rs | 613 +----------------- crates/bevy_input_focus/src/lib.rs | 3 + crates/bevy_input_focus/src/navigator.rs | 396 +++++++++++ examples/ui/auto_directional_navigation.rs | 10 +- 5 files changed, 645 insertions(+), 586 deletions(-) create mode 100644 crates/bevy_input_focus/src/auto_directional_navigation.rs create mode 100644 crates/bevy_input_focus/src/navigator.rs diff --git a/crates/bevy_input_focus/src/auto_directional_navigation.rs b/crates/bevy_input_focus/src/auto_directional_navigation.rs new file mode 100644 index 0000000000000..56b00daf2132d --- /dev/null +++ b/crates/bevy_input_focus/src/auto_directional_navigation.rs @@ -0,0 +1,209 @@ +//! An optional but recommended automatic directional navigation system, powered by +//! the [`AutoDirectionalNavigation`] component. +//! Prerequisites: Must have the `bevy_camera` and `bevy_ui` features enabled. + +use alloc::vec::Vec; +use bevy_camera::visibility::InheritedVisibility; +use bevy_ecs::{prelude::*, system::SystemParam}; +use bevy_math::CompassOctant; +use bevy_ui::{ComputedNode, ComputedUiTargetCamera, UiGlobalTransform}; + +use crate::navigator::{find_best_candidate, FocusableArea, NavigatorConfig}; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{prelude::*, Reflect}; + +/// Marker component to enable automatic directional navigation to and from the entity. +/// +/// Simply add this component to your UI entities so that the navigation algorithm will +/// consider this entity in its calculations: +/// +/// ```rust +/// # use bevy_ecs::prelude::*; +/// # use bevy_input_focus::directional_navigation::AutoDirectionalNavigation; +/// fn spawn_auto_nav_button(mut commands: Commands) { +/// commands.spawn(( +/// // ... Button, Node, etc. ... +/// AutoDirectionalNavigation::default(), // That's it! +/// )); +/// } +/// ``` +/// +/// # Multi-Layer UIs and Z-Index +/// +/// **Important**: Automatic navigation is currently **z-index agnostic** and treats +/// all entities with `AutoDirectionalNavigation` as a flat set, regardless of which UI layer +/// or z-index they belong to. This means navigation may jump between different layers (e.g., +/// from a background menu to an overlay popup). +/// +/// **Workarounds** for multi-layer UIs: +/// +/// 1. **Per-layer manual edge generation**: Query entities by layer and call +/// [`auto_generate_navigation_edges()`] separately for each layer: +/// ```rust,ignore +/// for layer in &layers { +/// let nodes: Vec = query_layer(layer).collect(); +/// auto_generate_navigation_edges(&mut nav_map, &nodes, &config); +/// } +/// ``` +/// +/// 2. **Manual cross-layer navigation**: Use [`DirectionalNavigationMap::add_edge()`] +/// to define explicit connections between layers (e.g., "Back" button to main menu). +/// +/// 3. **Remove component when layer is hidden**: Dynamically add/remove +/// `AutoDirectionalNavigation` based on which layers are currently active. +/// +/// See issue [#21679](https://github.com/bevyengine/bevy/issues/21679) for planned +/// improvements to layer-aware automatic navigation. +/// +/// # Opting Out +/// +/// To disable automatic navigation for specific entities: +/// +/// - **Remove the component**: Simply don't add `AutoDirectionalNavigation` to entities +/// that should only use manual navigation edges. +/// - **Dynamically toggle**: Remove/insert the component at runtime to enable/disable +/// automatic navigation as needed. +/// +/// Manual edges defined via [`DirectionalNavigationMap`] are completely independent and +/// will continue to work regardless of this component. +/// +/// # Requirements (for `bevy_ui`) +/// +/// Entities must also have: +/// - [`ComputedNode`] - for size information +/// - [`UiGlobalTransform`] - for position information +/// +/// These are automatically added by `bevy_ui` when you spawn UI entities. +/// +/// # Custom UI Systems +/// +/// For custom UI frameworks, you can call [`auto_generate_navigation_edges`] directly +/// in your own system instead of using this component. +#[derive(Component, Default, Debug, Clone, Copy, PartialEq)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Component, Default, Debug, PartialEq, Clone) +)] +pub struct AutoDirectionalNavigation { + /// Whether to also consider `TabIndex` for navigation order hints. + /// Currently unused but reserved for future functionality. + pub respect_tab_order: bool, +} + +/// A system parameter for auto navigating between focusable entities in a directional way. +#[derive(SystemParam, Debug)] +pub(crate) struct AutoDirectionalNavigator<'w, 's> { + /// Configuration for the automatic navigation system + pub config: Res<'w, NavigatorConfig>, + /// The entities which can possibly be navigated to automatically. + navigable_entities_query: Query< + 'w, + 's, + ( + Entity, + &'static ComputedUiTargetCamera, + &'static ComputedNode, + &'static UiGlobalTransform, + &'static InheritedVisibility, + ), + With, + >, + /// A query used to get the target camera and the [`FocusableArea`] for a given entity to be used in automatic navigation. + camera_and_focusable_area_query: Query< + 'w, + 's, + ( + Entity, + &'static ComputedUiTargetCamera, + &'static ComputedNode, + &'static UiGlobalTransform, + ), + With, + >, +} + +impl<'w, 's> AutoDirectionalNavigator<'w, 's> { + /// Tries to find the neighbor in a given direction from the given entity. Assumes the entity is valid. + /// + /// Returns a neighbor if successful. + /// Returns None if there is no neighbor in the requested direction. + pub fn get_neighbor( + &mut self, + from_entity: Entity, + direction: CompassOctant, + ) -> Option { + if let Some((target_camera, origin)) = self.entity_to_camera_and_focusable_area(from_entity) + && let Some(new_focus) = find_best_candidate( + &origin, + direction, + &self.get_navigable_nodes(target_camera), + &self.config, + ) + { + Some(new_focus) + } else { + None + } + } + + /// Returns a vec of [`FocusableArea`] representing nodes that are eligible to be automatically navigated to. + /// The camera of any navigable nodes will equal the desired `target_camera`. + fn get_navigable_nodes(&self, target_camera: Entity) -> Vec { + self.navigable_entities_query + .iter() + .filter_map( + |(entity, computed_target_camera, computed, transform, inherited_visibility)| { + // Skip hidden or zero-size nodes + if computed.is_empty() || !inherited_visibility.get() { + return None; + } + // Accept nodes that have the same target camera as the desired target camera + if let Some(tc) = computed_target_camera.get() + && tc == target_camera + { + let (_scale, _rotation, translation) = + transform.to_scale_angle_translation(); + Some(FocusableArea { + entity, + position: translation * computed.inverse_scale_factor(), + size: computed.size() * computed.inverse_scale_factor(), + }) + } else { + // The node either does not have a target camera or it is not the same as the desired one. + None + } + }, + ) + .collect() + } + + /// Gets the target camera and the [`FocusableArea`] of the provided entity, if it exists. + /// + /// Returns None if there was a [`QueryEntityError`](bevy_ecs::query::QueryEntityError) or + /// if the entity does not have a target camera. + fn entity_to_camera_and_focusable_area( + &self, + entity: Entity, + ) -> Option<(Entity, FocusableArea)> { + self.camera_and_focusable_area_query.get(entity).map_or( + None, + |(entity, computed_target_camera, computed, transform)| { + if let Some(target_camera) = computed_target_camera.get() { + let (_scale, _rotation, translation) = transform.to_scale_angle_translation(); + Some(( + target_camera, + FocusableArea { + entity, + position: translation * computed.inverse_scale_factor(), + size: computed.size() * computed.inverse_scale_factor(), + }, + )) + } else { + None + } + }, + ) + } +} diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index 4c47f92ae959c..b4e2621793cef 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -21,8 +21,10 @@ //! //! ## Automatic Navigation (Recommended) //! -//! The easiest way to set up navigation is to add the [`AutoDirectionalNavigation`] component -//! to your UI entities. The system will automatically compute the nearest neighbor in each direction +//! The easiest way to set up navigation is to add the +//! [`AutoDirectionalNavigation`](crate::auto_directional_navigation::AutoDirectionalNavigation) component +//! to your UI entities. You must have the `bevy_camera` and `bevy_ui` feature enabled to leverage this navigation. +//! The system will automatically compute the nearest neighbor in each direction //! based on position and size: //! //! ```rust,no_run @@ -58,19 +60,20 @@ //! - **Cross-layer navigation**: Connect elements across different UI layers or z-index levels //! - **Custom behavior**: Implement domain-specific navigation patterns (e.g., spreadsheet-style wrapping) -use alloc::vec::Vec; use bevy_app::prelude::*; -use bevy_camera::visibility::InheritedVisibility; use bevy_ecs::{ entity::{EntityHashMap, EntityHashSet}, prelude::*, system::SystemParam, }; -use bevy_math::{CompassOctant, Dir2, Rect, Vec2}; -use bevy_ui::{ComputedNode, ComputedUiTargetCamera, UiGlobalTransform}; +use bevy_math::{CompassOctant, Vec2}; use thiserror::Error; -use crate::InputFocus; +use crate::auto_directional_navigation::AutoDirectionalNavigator; +use crate::{ + navigator::{find_best_candidate, FocusableArea, NavigatorConfig}, + InputFocus, +}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{prelude::*, Reflect}; @@ -82,157 +85,7 @@ pub struct DirectionalNavigationPlugin; impl Plugin for DirectionalNavigationPlugin { fn build(&self, app: &mut App) { app.init_resource::() - .init_resource::(); - } -} - -/// Marker component to enable automatic directional navigation to and from the entity. -/// -/// Simply add this component to your UI entities so that the navigation algorithm will -/// consider this entity in its calculations: -/// -/// ```rust -/// # use bevy_ecs::prelude::*; -/// # use bevy_input_focus::directional_navigation::AutoDirectionalNavigation; -/// fn spawn_auto_nav_button(mut commands: Commands) { -/// commands.spawn(( -/// // ... Button, Node, etc. ... -/// AutoDirectionalNavigation::default(), // That's it! -/// )); -/// } -/// ``` -/// -/// # Multi-Layer UIs and Z-Index -/// -/// **Important**: Automatic navigation is currently **z-index agnostic** and treats -/// all entities with `AutoDirectionalNavigation` as a flat set, regardless of which UI layer -/// or z-index they belong to. This means navigation may jump between different layers (e.g., -/// from a background menu to an overlay popup). -/// -/// **Workarounds** for multi-layer UIs: -/// -/// 1. **Per-layer manual edge generation**: Query entities by layer and call -/// [`auto_generate_navigation_edges()`] separately for each layer: -/// ```rust,ignore -/// for layer in &layers { -/// let nodes: Vec = query_layer(layer).collect(); -/// auto_generate_navigation_edges(&mut nav_map, &nodes, &config); -/// } -/// ``` -/// -/// 2. **Manual cross-layer navigation**: Use [`DirectionalNavigationMap::add_edge()`] -/// to define explicit connections between layers (e.g., "Back" button to main menu). -/// -/// 3. **Remove component when layer is hidden**: Dynamically add/remove -/// `AutoDirectionalNavigation` based on which layers are currently active. -/// -/// See issue [#21679](https://github.com/bevyengine/bevy/issues/21679) for planned -/// improvements to layer-aware automatic navigation. -/// -/// # Opting Out -/// -/// To disable automatic navigation for specific entities: -/// -/// - **Remove the component**: Simply don't add `AutoDirectionalNavigation` to entities -/// that should only use manual navigation edges. -/// - **Dynamically toggle**: Remove/insert the component at runtime to enable/disable -/// automatic navigation as needed. -/// -/// Manual edges defined via [`DirectionalNavigationMap`] are completely independent and -/// will continue to work regardless of this component. -/// -/// # Requirements (for `bevy_ui`) -/// -/// Entities must also have: -/// - [`ComputedNode`] - for size information -/// - [`UiGlobalTransform`] - for position information -/// -/// These are automatically added by `bevy_ui` when you spawn UI entities. -/// -/// # Custom UI Systems -/// -/// For custom UI frameworks, you can call [`auto_generate_navigation_edges`] directly -/// in your own system instead of using this component. -#[derive(Component, Default, Debug, Clone, Copy, PartialEq)] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Component, Default, Debug, PartialEq, Clone) -)] -pub struct AutoDirectionalNavigation { - /// Whether to also consider `TabIndex` for navigation order hints. - /// Currently unused but reserved for future functionality. - pub respect_tab_order: bool, -} - -/// Configuration resource for automatic navigation. -/// -/// This resource controls how the automatic navigation system computes which -/// nodes should be connected in each direction. -#[derive(Resource, Debug, Clone, PartialEq)] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Resource, Debug, PartialEq, Clone) -)] -pub struct AutoNavigationConfig { - /// Minimum overlap ratio (0.0-1.0) required along the perpendicular axis for cardinal directions. - /// - /// This parameter controls how much two UI elements must overlap in the perpendicular direction - /// to be considered reachable neighbors. It only applies to cardinal directions (`North`, `South`, `East`, `West`); - /// diagonal directions (`NorthEast`, `SouthEast`, etc.) ignore this requirement entirely. - /// - /// # Calculation - /// - /// The overlap factor is calculated as: - /// ```text - /// overlap_factor = actual_overlap / min(origin_size, candidate_size) - /// ``` - /// - /// For East/West navigation, this measures vertical overlap: - /// - `actual_overlap` = overlapping height between the two elements - /// - Sizes are the heights of the origin and candidate - /// - /// For North/South navigation, this measures horizontal overlap: - /// - `actual_overlap` = overlapping width between the two elements - /// - Sizes are the widths of the origin and candidate - /// - /// # Examples - /// - /// - `0.0` (default): Any overlap is sufficient. Even if elements barely touch, they can be neighbors. - /// - `0.5`: Elements must overlap by at least 50% of the smaller element's size. - /// - `1.0`: Perfect alignment required. The smaller element must be completely within the bounds - /// of the larger element along the perpendicular axis. - /// - /// # Use Cases - /// - /// - **Sparse/irregular layouts** (e.g., star constellations): Use `0.0` to allow navigation - /// between elements that don't directly align. - /// - **Grid layouts**: Use `0.5` or higher to ensure navigation only connects elements in - /// the same row or column. - /// - **Strict alignment**: Use `1.0` to require perfect alignment, though this may result - /// in disconnected navigation graphs if elements aren't precisely aligned. - pub min_alignment_factor: f32, - - /// Maximum search distance in logical pixels. - /// - /// Nodes beyond this distance won't be connected. `None` means unlimited. - pub max_search_distance: Option, - - /// Whether to prefer nodes that are more aligned with the exact direction. - /// - /// When `true`, nodes that are more directly in line with the requested direction - /// will be strongly preferred over nodes at an angle. - pub prefer_aligned: bool, -} - -impl Default for AutoNavigationConfig { - fn default() -> Self { - Self { - min_alignment_factor: 0.0, // Any overlap is acceptable - max_search_distance: None, // No distance limit - prefer_aligned: true, // Prefer well-aligned nodes - } + .init_resource::(); } } @@ -410,33 +263,8 @@ pub struct DirectionalNavigation<'w, 's> { pub focus: ResMut<'w, InputFocus>, /// The directional navigation map containing manually defined connections between entities. pub map: Res<'w, DirectionalNavigationMap>, - /// Configuration for the automated portion of the navigation algorithm. - pub config: Res<'w, AutoNavigationConfig>, - /// The entities which can possibly be navigated to automatically. - navigable_entities_query: Query< - 'w, - 's, - ( - Entity, - &'static ComputedUiTargetCamera, - &'static ComputedNode, - &'static UiGlobalTransform, - &'static InheritedVisibility, - ), - With, - >, - /// A query used to get the target camera and the [`FocusableArea`] for a given entity to be used in automatic navigation. - camera_and_focusable_area_query: Query< - 'w, - 's, - ( - Entity, - &'static ComputedUiTargetCamera, - &'static ComputedNode, - &'static UiGlobalTransform, - ), - With, - >, + /// The system param that holds our automatic navigation system logic. + pub(crate) auto_directional_navigator: AutoDirectionalNavigator<'w, 's>, } impl<'w, 's> DirectionalNavigation<'w, 's> { @@ -454,86 +282,25 @@ impl<'w, 's> DirectionalNavigation<'w, 's> { // Respect manual edges first if let Some(new_focus) = self.map.get_neighbor(current_focus, direction) { self.focus.set(new_focus); - Ok(new_focus) - } else if let Some((target_camera, origin)) = - self.entity_to_camera_and_focusable_area(current_focus) - && let Some(new_focus) = find_best_candidate( - &origin, - direction, - &self.get_navigable_nodes(target_camera), - &self.config, - ) + return Ok(new_focus); + } + + // If automatic navigation is enabled, try to get the result there as well. + if let Some(new_focus) = self + .auto_directional_navigator + .get_neighbor(current_focus, direction) { self.focus.set(new_focus); - Ok(new_focus) - } else { - Err(DirectionalNavigationError::NoNeighborInDirection { - current_focus, - direction, - }) + return Ok(new_focus); } - } else { - Err(DirectionalNavigationError::NoFocus) - } - } - /// Returns a vec of [`FocusableArea`] representing nodes that are eligible to be automatically navigated to. - /// The camera of any navigable nodes will equal the desired `target_camera`. - fn get_navigable_nodes(&self, target_camera: Entity) -> Vec { - self.navigable_entities_query - .iter() - .filter_map( - |(entity, computed_target_camera, computed, transform, inherited_visibility)| { - // Skip hidden or zero-size nodes - if computed.is_empty() || !inherited_visibility.get() { - return None; - } - // Accept nodes that have the same target camera as the desired target camera - if let Some(tc) = computed_target_camera.get() - && tc == target_camera - { - let (_scale, _rotation, translation) = - transform.to_scale_angle_translation(); - Some(FocusableArea { - entity, - position: translation * computed.inverse_scale_factor(), - size: computed.size() * computed.inverse_scale_factor(), - }) - } else { - // The node either does not have a target camera or it is not the same as the desired one. - None - } - }, - ) - .collect() - } + return Err(DirectionalNavigationError::NoNeighborInDirection { + current_focus, + direction, + }); + } - /// Gets the target camera and the [`FocusableArea`] of the provided entity, if it exists. - /// - /// Returns None if there was a [`QueryEntityError`](bevy_ecs::query::QueryEntityError) or - /// if the entity does not have a target camera. - fn entity_to_camera_and_focusable_area( - &self, - entity: Entity, - ) -> Option<(Entity, FocusableArea)> { - self.camera_and_focusable_area_query.get(entity).map_or( - None, - |(entity, computed_target_camera, computed, transform)| { - if let Some(target_camera) = computed_target_camera.get() { - let (_scale, _rotation, translation) = transform.to_scale_angle_translation(); - Some(( - target_camera, - FocusableArea { - entity, - position: translation * computed.inverse_scale_factor(), - size: computed.size() * computed.inverse_scale_factor(), - }, - )) - } else { - None - } - }, - ) + Err(DirectionalNavigationError::NoFocus) } } @@ -553,27 +320,6 @@ pub enum DirectionalNavigationError { }, } -/// A focusable area with position and size information. -/// -/// This struct represents a UI element used during automatic directional navigation, -/// containing its entity ID, center position, and size for spatial navigation calculations. -/// -/// The term "focusable area" avoids confusion with UI [`Node`](bevy_ui::Node) components. -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Clone) -)] -pub struct FocusableArea { - /// The entity identifier for this focusable area. - pub entity: Entity, - /// The center position in global coordinates. - pub position: Vec2, - /// The size (width, height) of the area. - pub size: Vec2, -} - /// Trait for extracting position and size from navigable UI components. /// /// This allows the auto-navigation system to work with different UI implementations @@ -583,178 +329,6 @@ pub trait Navigable { fn get_bounds(&self) -> (Vec2, Vec2); } -// We can't directly implement this for `bevy_ui` types here without circular dependencies, -// so we'll use a more generic approach with separate functions for different component sets. - -/// Calculate 1D overlap between two ranges. -/// -/// Returns a value between 0.0 (no overlap) and 1.0 (perfect overlap). -fn calculate_1d_overlap( - origin_pos: f32, - origin_size: f32, - candidate_pos: f32, - candidate_size: f32, -) -> f32 { - let origin_min = origin_pos - origin_size / 2.0; - let origin_max = origin_pos + origin_size / 2.0; - let cand_min = candidate_pos - candidate_size / 2.0; - let cand_max = candidate_pos + candidate_size / 2.0; - - let overlap = (origin_max.min(cand_max) - origin_min.max(cand_min)).max(0.0); - let max_overlap = origin_size.min(candidate_size); - if max_overlap > 0.0 { - overlap / max_overlap - } else { - 0.0 - } -} - -/// Calculate the overlap factor between two nodes in the perpendicular axis. -/// -/// Returns a value between 0.0 (no overlap) and 1.0 (perfect overlap). -/// For diagonal directions, always returns 1.0. -fn calculate_overlap( - origin_pos: Vec2, - origin_size: Vec2, - candidate_pos: Vec2, - candidate_size: Vec2, - octant: CompassOctant, -) -> f32 { - match octant { - CompassOctant::North | CompassOctant::South => { - // Check horizontal overlap - calculate_1d_overlap( - origin_pos.x, - origin_size.x, - candidate_pos.x, - candidate_size.x, - ) - } - CompassOctant::East | CompassOctant::West => { - // Check vertical overlap - calculate_1d_overlap( - origin_pos.y, - origin_size.y, - candidate_pos.y, - candidate_size.y, - ) - } - // Diagonal directions don't require strict overlap - _ => 1.0, - } -} - -/// Score a candidate node for navigation in a given direction. -/// -/// Lower score is better. Returns `f32::INFINITY` for unreachable nodes. -fn score_candidate( - origin_pos: Vec2, - origin_size: Vec2, - candidate_pos: Vec2, - candidate_size: Vec2, - octant: CompassOctant, - config: &AutoNavigationConfig, -) -> f32 { - // Get direction in mathematical coordinates, then flip Y for UI coordinates - let dir = Dir2::from(octant).as_vec2() * Vec2::new(1.0, -1.0); - let to_candidate = candidate_pos - origin_pos; - - // Check direction first - // Convert UI coordinates (Y+ = down) to mathematical coordinates (Y+ = up) by flipping Y - let origin_math = Vec2::new(origin_pos.x, -origin_pos.y); - let candidate_math = Vec2::new(candidate_pos.x, -candidate_pos.y); - if !octant.is_in_direction(origin_math, candidate_math) { - return f32::INFINITY; - } - - // Check overlap for cardinal directions - let overlap_factor = calculate_overlap( - origin_pos, - origin_size, - candidate_pos, - candidate_size, - octant, - ); - - if overlap_factor < config.min_alignment_factor { - return f32::INFINITY; - } - - // Calculate distance between rectangle edges, not centers - let origin_rect = Rect::from_center_size(origin_pos, origin_size); - let candidate_rect = Rect::from_center_size(candidate_pos, candidate_size); - let dx = (candidate_rect.min.x - origin_rect.max.x) - .max(origin_rect.min.x - candidate_rect.max.x) - .max(0.0); - let dy = (candidate_rect.min.y - origin_rect.max.y) - .max(origin_rect.min.y - candidate_rect.max.y) - .max(0.0); - let distance = (dx * dx + dy * dy).sqrt(); - - // Check max distance - if let Some(max_dist) = config.max_search_distance { - if distance > max_dist { - return f32::INFINITY; - } - } - - // Calculate alignment score using center-to-center direction - let center_distance = to_candidate.length(); - let alignment = if center_distance > 0.0 { - to_candidate.normalize().dot(dir).max(0.0) - } else { - 1.0 - }; - - // Combine distance and alignment - // Prefer aligned nodes by penalizing misalignment - let alignment_penalty = if config.prefer_aligned { - (1.0 - alignment) * distance * 2.0 // Misalignment scales with distance - } else { - 0.0 - }; - - distance + alignment_penalty -} - -/// Finds the best entity to navigate to from the origin towards the given direction. -/// -/// For details on what "best" means here, refer to [`AutoNavigationConfig`]. -fn find_best_candidate( - origin: &FocusableArea, - direction: CompassOctant, - candidates: &[FocusableArea], - config: &AutoNavigationConfig, -) -> Option { - // Find best candidate in this direction - let mut best_candidate = None; - let mut best_score = f32::INFINITY; - - for candidate in candidates { - // Skip self - if candidate.entity == origin.entity { - continue; - } - - // Score the candidate - let score = score_candidate( - origin.position, - origin.size, - candidate.position, - candidate.size, - direction, - config, - ); - - if score < best_score { - best_score = score; - best_candidate = Some(candidate.entity); - } - } - - best_candidate -} - /// Automatically generates directional navigation edges for a collection of nodes. /// /// This function takes a slice of navigation nodes with their positions and sizes, and populates @@ -774,7 +348,7 @@ fn find_best_candidate( /// # use bevy_ecs::entity::Entity; /// # use bevy_math::Vec2; /// let mut nav_map = DirectionalNavigationMap::default(); -/// let config = AutoNavigationConfig::default(); +/// let config = NavigatorConfig::default(); /// /// let nodes = vec![ /// FocusableArea { entity: Entity::PLACEHOLDER, position: Vec2::new(100.0, 100.0), size: Vec2::new(50.0, 50.0) }, @@ -786,7 +360,7 @@ fn find_best_candidate( pub fn auto_generate_navigation_edges( nav_map: &mut DirectionalNavigationMap, nodes: &[FocusableArea], - config: &AutoNavigationConfig, + config: &NavigatorConfig, ) { // For each node, find best neighbor in each direction for origin in nodes { @@ -973,7 +547,7 @@ mod tests { focus.set(a); world.insert_resource(focus); - let config = AutoNavigationConfig::default(); + let config = NavigatorConfig::default(); world.insert_resource(config); assert_eq!(world.resource::().get(), Some(a)); @@ -992,131 +566,10 @@ mod tests { assert_eq!(world.resource::().get(), Some(a)); } - // Tests for automatic navigation helpers - #[test] - fn test_is_in_direction() { - let origin = Vec2::new(100.0, 100.0); - - // Node to the north (mathematically up) should have larger Y - let north_node = Vec2::new(100.0, 150.0); - assert!(CompassOctant::North.is_in_direction(origin, north_node)); - assert!(!CompassOctant::South.is_in_direction(origin, north_node)); - - // Node to the south (mathematically down) should have smaller Y - let south_node = Vec2::new(100.0, 50.0); - assert!(CompassOctant::South.is_in_direction(origin, south_node)); - assert!(!CompassOctant::North.is_in_direction(origin, south_node)); - - // Node to the east should be in East direction - let east_node = Vec2::new(150.0, 100.0); - assert!(CompassOctant::East.is_in_direction(origin, east_node)); - assert!(!CompassOctant::West.is_in_direction(origin, east_node)); - - // Node to the northeast (mathematically up-right) should have larger Y, larger X - let ne_node = Vec2::new(150.0, 150.0); - assert!(CompassOctant::NorthEast.is_in_direction(origin, ne_node)); - assert!(!CompassOctant::SouthWest.is_in_direction(origin, ne_node)); - } - - #[test] - fn test_calculate_overlap_horizontal() { - let origin_pos = Vec2::new(100.0, 100.0); - let origin_size = Vec2::new(50.0, 50.0); - - // Fully overlapping node to the north - let north_pos = Vec2::new(100.0, 200.0); - let north_size = Vec2::new(50.0, 50.0); - let overlap = calculate_overlap( - origin_pos, - origin_size, - north_pos, - north_size, - CompassOctant::North, - ); - assert_eq!(overlap, 1.0); // Full overlap - - // Partially overlapping node to the north - let north_pos = Vec2::new(110.0, 200.0); - let partial_overlap = calculate_overlap( - origin_pos, - origin_size, - north_pos, - north_size, - CompassOctant::North, - ); - assert!(partial_overlap > 0.0 && partial_overlap < 1.0); - - // No overlap - let north_pos = Vec2::new(200.0, 200.0); - let no_overlap = calculate_overlap( - origin_pos, - origin_size, - north_pos, - north_size, - CompassOctant::North, - ); - assert_eq!(no_overlap, 0.0); - } - - #[test] - fn test_score_candidate() { - let config = AutoNavigationConfig::default(); - let origin_pos = Vec2::new(100.0, 100.0); - let origin_size = Vec2::new(50.0, 50.0); - - // Node directly to the north (up on screen = smaller Y) - let north_pos = Vec2::new(100.0, 0.0); - let north_size = Vec2::new(50.0, 50.0); - let north_score = score_candidate( - origin_pos, - origin_size, - north_pos, - north_size, - CompassOctant::North, - &config, - ); - assert!(north_score < f32::INFINITY); - assert!(north_score < 150.0); // Should be close to the distance (100) - - // Node in opposite direction (should be unreachable) - let south_pos = Vec2::new(100.0, 200.0); - let south_size = Vec2::new(50.0, 50.0); - let invalid_score = score_candidate( - origin_pos, - origin_size, - south_pos, - south_size, - CompassOctant::North, - &config, - ); - assert_eq!(invalid_score, f32::INFINITY); - - // Closer node should have better score than farther node - let close_pos = Vec2::new(100.0, 50.0); - let far_pos = Vec2::new(100.0, -100.0); - let close_score = score_candidate( - origin_pos, - origin_size, - close_pos, - north_size, - CompassOctant::North, - &config, - ); - let far_score = score_candidate( - origin_pos, - origin_size, - far_pos, - north_size, - CompassOctant::North, - &config, - ); - assert!(close_score < far_score); - } - #[test] fn test_auto_generate_navigation_edges() { let mut nav_map = DirectionalNavigationMap::default(); - let config = AutoNavigationConfig::default(); + let config = NavigatorConfig::default(); // Create a 2x2 grid of nodes (using UI coordinates: smaller Y = higher on screen) let node_a = Entity::from_bits(1); // Top-left @@ -1179,7 +632,7 @@ mod tests { #[test] fn test_auto_generate_respects_manual_edges() { let mut nav_map = DirectionalNavigationMap::default(); - let config = AutoNavigationConfig::default(); + let config = NavigatorConfig::default(); let node_a = Entity::from_bits(1); let node_b = Entity::from_bits(2); @@ -1218,7 +671,7 @@ mod tests { #[test] fn test_edge_distance_vs_center_distance() { let mut nav_map = DirectionalNavigationMap::default(); - let config = AutoNavigationConfig::default(); + let config = NavigatorConfig::default(); let left = Entity::from_bits(1); let wide_top = Entity::from_bits(2); diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index fac16f62209bd..c1ac1f2e8aec9 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -22,6 +22,9 @@ extern crate std; extern crate alloc; +mod navigator; +pub use navigator::*; +pub mod auto_directional_navigation; pub mod directional_navigation; pub mod tab_navigation; diff --git a/crates/bevy_input_focus/src/navigator.rs b/crates/bevy_input_focus/src/navigator.rs new file mode 100644 index 0000000000000..3a7d90bc8d7ba --- /dev/null +++ b/crates/bevy_input_focus/src/navigator.rs @@ -0,0 +1,396 @@ +//! Common structs and functions used in both automatic navigators and manual navigators. + +use bevy_ecs::prelude::*; +use bevy_math::{CompassOctant, Dir2, Rect, Vec2}; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; + +/// Configuration resource for automatic directional navigation and for generating manual +/// navigation edges via [`auto_generate_navigation_edges`](crate::directional_navigation::auto_generate_navigation_edges) +/// +/// This resource controls how nodes should be automatically connected in each direction. +#[derive(Resource, Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Resource, Debug, PartialEq, Clone) +)] +pub struct NavigatorConfig { + /// Minimum overlap ratio (0.0-1.0) required along the perpendicular axis for cardinal directions. + /// + /// This parameter controls how much two UI elements must overlap in the perpendicular direction + /// to be considered reachable neighbors. It only applies to cardinal directions (`North`, `South`, `East`, `West`); + /// diagonal directions (`NorthEast`, `SouthEast`, etc.) ignore this requirement entirely. + /// + /// # Calculation + /// + /// The overlap factor is calculated as: + /// ```text + /// overlap_factor = actual_overlap / min(origin_size, candidate_size) + /// ``` + /// + /// For East/West navigation, this measures vertical overlap: + /// - `actual_overlap` = overlapping height between the two elements + /// - Sizes are the heights of the origin and candidate + /// + /// For North/South navigation, this measures horizontal overlap: + /// - `actual_overlap` = overlapping width between the two elements + /// - Sizes are the widths of the origin and candidate + /// + /// # Examples + /// + /// - `0.0` (default): Any overlap is sufficient. Even if elements barely touch, they can be neighbors. + /// - `0.5`: Elements must overlap by at least 50% of the smaller element's size. + /// - `1.0`: Perfect alignment required. The smaller element must be completely within the bounds + /// of the larger element along the perpendicular axis. + /// + /// # Use Cases + /// + /// - **Sparse/irregular layouts** (e.g., star constellations): Use `0.0` to allow navigation + /// between elements that don't directly align. + /// - **Grid layouts**: Use `0.5` or higher to ensure navigation only connects elements in + /// the same row or column. + /// - **Strict alignment**: Use `1.0` to require perfect alignment, though this may result + /// in disconnected navigation graphs if elements aren't precisely aligned. + pub min_alignment_factor: f32, + + /// Maximum search distance in logical pixels. + /// + /// Nodes beyond this distance won't be connected. `None` means unlimited. + pub max_search_distance: Option, + + /// Whether to prefer nodes that are more aligned with the exact direction. + /// + /// When `true`, nodes that are more directly in line with the requested direction + /// will be strongly preferred over nodes at an angle. + pub prefer_aligned: bool, +} + +impl Default for NavigatorConfig { + fn default() -> Self { + Self { + min_alignment_factor: 0.0, // Any overlap is acceptable + max_search_distance: None, // No distance limit + prefer_aligned: true, // Prefer well-aligned nodes + } + } +} + +/// A focusable area with position and size information. +/// +/// This struct represents a UI element used during directional navigation, +/// containing its entity ID, center position, and size for spatial navigation calculations. +/// +/// The term "focusable area" avoids confusion with UI [`Node`](bevy_ui::Node) components. +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +pub struct FocusableArea { + /// The entity identifier for this focusable area. + pub entity: Entity, + /// The center position in global coordinates. + pub position: Vec2, + /// The size (width, height) of the area. + pub size: Vec2, +} + +// We can't directly implement this for `bevy_ui` types here without circular dependencies, +// so we'll use a more generic approach with separate functions for different component sets. + +/// Calculate 1D overlap between two ranges. +/// +/// Returns a value between 0.0 (no overlap) and 1.0 (perfect overlap). +fn calculate_1d_overlap( + origin_pos: f32, + origin_size: f32, + candidate_pos: f32, + candidate_size: f32, +) -> f32 { + let origin_min = origin_pos - origin_size / 2.0; + let origin_max = origin_pos + origin_size / 2.0; + let cand_min = candidate_pos - candidate_size / 2.0; + let cand_max = candidate_pos + candidate_size / 2.0; + + let overlap = (origin_max.min(cand_max) - origin_min.max(cand_min)).max(0.0); + let max_overlap = origin_size.min(candidate_size); + if max_overlap > 0.0 { + overlap / max_overlap + } else { + 0.0 + } +} + +/// Calculate the overlap factor between two nodes in the perpendicular axis. +/// +/// Returns a value between 0.0 (no overlap) and 1.0 (perfect overlap). +/// For diagonal directions, always returns 1.0. +fn calculate_overlap( + origin_pos: Vec2, + origin_size: Vec2, + candidate_pos: Vec2, + candidate_size: Vec2, + octant: CompassOctant, +) -> f32 { + match octant { + CompassOctant::North | CompassOctant::South => { + // Check horizontal overlap + calculate_1d_overlap( + origin_pos.x, + origin_size.x, + candidate_pos.x, + candidate_size.x, + ) + } + CompassOctant::East | CompassOctant::West => { + // Check vertical overlap + calculate_1d_overlap( + origin_pos.y, + origin_size.y, + candidate_pos.y, + candidate_size.y, + ) + } + // Diagonal directions don't require strict overlap + _ => 1.0, + } +} + +/// Score a candidate node for navigation in a given direction. +/// +/// Lower score is better. Returns `f32::INFINITY` for unreachable nodes. +fn score_candidate( + origin_pos: Vec2, + origin_size: Vec2, + candidate_pos: Vec2, + candidate_size: Vec2, + octant: CompassOctant, + config: &NavigatorConfig, +) -> f32 { + // Get direction in mathematical coordinates, then flip Y for UI coordinates + let dir = Dir2::from(octant).as_vec2() * Vec2::new(1.0, -1.0); + let to_candidate = candidate_pos - origin_pos; + + // Check direction first + // Convert UI coordinates (Y+ = down) to mathematical coordinates (Y+ = up) by flipping Y + let origin_math = Vec2::new(origin_pos.x, -origin_pos.y); + let candidate_math = Vec2::new(candidate_pos.x, -candidate_pos.y); + if !octant.is_in_direction(origin_math, candidate_math) { + return f32::INFINITY; + } + + // Check overlap for cardinal directions + let overlap_factor = calculate_overlap( + origin_pos, + origin_size, + candidate_pos, + candidate_size, + octant, + ); + + if overlap_factor < config.min_alignment_factor { + return f32::INFINITY; + } + + // Calculate distance between rectangle edges, not centers + let origin_rect = Rect::from_center_size(origin_pos, origin_size); + let candidate_rect = Rect::from_center_size(candidate_pos, candidate_size); + let dx = (candidate_rect.min.x - origin_rect.max.x) + .max(origin_rect.min.x - candidate_rect.max.x) + .max(0.0); + let dy = (candidate_rect.min.y - origin_rect.max.y) + .max(origin_rect.min.y - candidate_rect.max.y) + .max(0.0); + let distance = (dx * dx + dy * dy).sqrt(); + + // Check max distance + if let Some(max_dist) = config.max_search_distance { + if distance > max_dist { + return f32::INFINITY; + } + } + + // Calculate alignment score using center-to-center direction + let center_distance = to_candidate.length(); + let alignment = if center_distance > 0.0 { + to_candidate.normalize().dot(dir).max(0.0) + } else { + 1.0 + }; + + // Combine distance and alignment + // Prefer aligned nodes by penalizing misalignment + let alignment_penalty = if config.prefer_aligned { + (1.0 - alignment) * distance * 2.0 // Misalignment scales with distance + } else { + 0.0 + }; + + distance + alignment_penalty +} + +/// Finds the best entity to navigate to from the origin towards the given direction. +/// +/// For details on what "best" means here, refer to [`NavigatorConfig`]. +pub(crate) fn find_best_candidate( + origin: &FocusableArea, + direction: CompassOctant, + candidates: &[FocusableArea], + config: &NavigatorConfig, +) -> Option { + // Find best candidate in this direction + let mut best_candidate = None; + let mut best_score = f32::INFINITY; + + for candidate in candidates { + // Skip self + if candidate.entity == origin.entity { + continue; + } + + // Score the candidate + let score = score_candidate( + origin.position, + origin.size, + candidate.position, + candidate.size, + direction, + config, + ); + + if score < best_score { + best_score = score; + best_candidate = Some(candidate.entity); + } + } + + best_candidate +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_in_direction() { + let origin = Vec2::new(100.0, 100.0); + + // Node to the north (mathematically up) should have larger Y + let north_node = Vec2::new(100.0, 150.0); + assert!(CompassOctant::North.is_in_direction(origin, north_node)); + assert!(!CompassOctant::South.is_in_direction(origin, north_node)); + + // Node to the south (mathematically down) should have smaller Y + let south_node = Vec2::new(100.0, 50.0); + assert!(CompassOctant::South.is_in_direction(origin, south_node)); + assert!(!CompassOctant::North.is_in_direction(origin, south_node)); + + // Node to the east should be in East direction + let east_node = Vec2::new(150.0, 100.0); + assert!(CompassOctant::East.is_in_direction(origin, east_node)); + assert!(!CompassOctant::West.is_in_direction(origin, east_node)); + + // Node to the northeast (mathematically up-right) should have larger Y, larger X + let ne_node = Vec2::new(150.0, 150.0); + assert!(CompassOctant::NorthEast.is_in_direction(origin, ne_node)); + assert!(!CompassOctant::SouthWest.is_in_direction(origin, ne_node)); + } + + #[test] + fn test_calculate_overlap_horizontal() { + let origin_pos = Vec2::new(100.0, 100.0); + let origin_size = Vec2::new(50.0, 50.0); + + // Fully overlapping node to the north + let north_pos = Vec2::new(100.0, 200.0); + let north_size = Vec2::new(50.0, 50.0); + let overlap = calculate_overlap( + origin_pos, + origin_size, + north_pos, + north_size, + CompassOctant::North, + ); + assert_eq!(overlap, 1.0); // Full overlap + + // Partially overlapping node to the north + let north_pos = Vec2::new(110.0, 200.0); + let partial_overlap = calculate_overlap( + origin_pos, + origin_size, + north_pos, + north_size, + CompassOctant::North, + ); + assert!(partial_overlap > 0.0 && partial_overlap < 1.0); + + // No overlap + let north_pos = Vec2::new(200.0, 200.0); + let no_overlap = calculate_overlap( + origin_pos, + origin_size, + north_pos, + north_size, + CompassOctant::North, + ); + assert_eq!(no_overlap, 0.0); + } + + #[test] + fn test_score_candidate() { + let config = NavigatorConfig::default(); + let origin_pos = Vec2::new(100.0, 100.0); + let origin_size = Vec2::new(50.0, 50.0); + + // Node directly to the north (up on screen = smaller Y) + let north_pos = Vec2::new(100.0, 0.0); + let north_size = Vec2::new(50.0, 50.0); + let north_score = score_candidate( + origin_pos, + origin_size, + north_pos, + north_size, + CompassOctant::North, + &config, + ); + assert!(north_score < f32::INFINITY); + assert!(north_score < 150.0); // Should be close to the distance (100) + + // Node in opposite direction (should be unreachable) + let south_pos = Vec2::new(100.0, 200.0); + let south_size = Vec2::new(50.0, 50.0); + let invalid_score = score_candidate( + origin_pos, + origin_size, + south_pos, + south_size, + CompassOctant::North, + &config, + ); + assert_eq!(invalid_score, f32::INFINITY); + + // Closer node should have better score than farther node + let close_pos = Vec2::new(100.0, 50.0); + let far_pos = Vec2::new(100.0, -100.0); + let close_score = score_candidate( + origin_pos, + origin_size, + close_pos, + north_size, + CompassOctant::North, + &config, + ); + let far_score = score_candidate( + origin_pos, + origin_size, + far_pos, + north_size, + CompassOctant::North, + &config, + ); + assert!(close_score < far_score); + } +} diff --git a/examples/ui/auto_directional_navigation.rs b/examples/ui/auto_directional_navigation.rs index 58dc14dbd6dd0..a3139760cb62d 100644 --- a/examples/ui/auto_directional_navigation.rs +++ b/examples/ui/auto_directional_navigation.rs @@ -17,11 +17,9 @@ use core::time::Duration; use bevy::{ camera::NormalizedRenderTarget, input_focus::{ - directional_navigation::{ - AutoDirectionalNavigation, AutoNavigationConfig, DirectionalNavigation, - DirectionalNavigationPlugin, - }, - InputDispatchPlugin, InputFocus, InputFocusVisible, + auto_directional_navigation::AutoDirectionalNavigation, + directional_navigation::{DirectionalNavigation, DirectionalNavigationPlugin}, + InputDispatchPlugin, InputFocus, InputFocusVisible, NavigatorConfig, }, math::{CompassOctant, Dir2}, picking::{ @@ -44,7 +42,7 @@ fn main() { // It starts as false, but we set it to true here as we would like to see the focus indicator .insert_resource(InputFocusVisible(true)) // Configure auto-navigation behavior - .insert_resource(AutoNavigationConfig { + .insert_resource(NavigatorConfig { // Require at least 10% overlap in perpendicular axis for cardinal directions min_alignment_factor: 0.1, // Don't connect nodes more than 500 pixels apart From 29dfd2a771c3a0303172b7db4e5a7f1456a75c89 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Thu, 1 Jan 2026 02:45:26 -0500 Subject: [PATCH 02/15] fix: makes bevy_ui and _camera optional deps for _input_focus --- Cargo.toml | 4 ++++ crates/bevy_input_focus/Cargo.toml | 7 +++++-- .../src/auto_directional_navigation.rs | 19 +++++++++++-------- .../src/directional_navigation.rs | 13 +++++++++++++ crates/bevy_input_focus/src/lib.rs | 1 + crates/bevy_internal/Cargo.toml | 2 ++ examples/ui/auto_directional_navigation.rs | 1 + 7 files changed, 37 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d5145a9347685..6481666c9f918 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -396,6 +396,9 @@ bevy_log = ["bevy_internal/bevy_log"] # Enable input focus subsystem bevy_input_focus = ["bevy_internal/bevy_input_focus"] +# Enable the automatic navigation subsystem in bevy_input_focus +auto_nav = ["bevy_internal/auto_nav"] + # Experimental headless widget collection for Bevy UI. experimental_bevy_ui_widgets = ["bevy_internal/bevy_ui_widgets"] @@ -5068,6 +5071,7 @@ name = "auto_directional_navigation" path = "examples/ui/auto_directional_navigation.rs" # Causes an ICE on docs.rs doc-scrape-examples = false +required-features = ["auto_nav"] [package.metadata.example.auto_directional_navigation] name = "Automatic Directional Navigation" diff --git a/crates/bevy_input_focus/Cargo.toml b/crates/bevy_input_focus/Cargo.toml index a469e006374bf..9db3a81298ece 100644 --- a/crates/bevy_input_focus/Cargo.toml +++ b/crates/bevy_input_focus/Cargo.toml @@ -42,6 +42,9 @@ serialize = [ "bevy_window/serialize", ] +## Adds auto navigation capability and enables its use +auto_nav = ["dep:bevy_camera", "dep:bevy_ui"] + # Platform Compatibility ## Allows access to the `std` crate. Enabling this feature will prevent compilation @@ -71,12 +74,12 @@ libm = ["bevy_math/libm", "bevy_window/libm"] [dependencies] # bevy bevy_app = { path = "../bevy_app", version = "0.18.0-dev", default-features = false } -bevy_camera = { path = "../bevy_camera", version = "0.18.0-dev", default-features = false } +bevy_camera = { path = "../bevy_camera", version = "0.18.0-dev", default-features = false, optional = true } bevy_ecs = { path = "../bevy_ecs", version = "0.18.0-dev", default-features = false } bevy_input = { path = "../bevy_input", version = "0.18.0-dev", default-features = false } bevy_math = { path = "../bevy_math", version = "0.18.0-dev", default-features = false } bevy_picking = { path = "../bevy_picking", version = "0.18.0-dev", default-features = false, optional = true } -bevy_ui = { path = "../bevy_ui", version = "0.18.0-dev", default-features = false } +bevy_ui = { path = "../bevy_ui", version = "0.18.0-dev", default-features = false, optional = true } bevy_window = { path = "../bevy_window", version = "0.18.0-dev", default-features = false } bevy_reflect = { path = "../bevy_reflect", version = "0.18.0-dev", features = [ "glam", diff --git a/crates/bevy_input_focus/src/auto_directional_navigation.rs b/crates/bevy_input_focus/src/auto_directional_navigation.rs index 56b00daf2132d..02bbf813d9d64 100644 --- a/crates/bevy_input_focus/src/auto_directional_navigation.rs +++ b/crates/bevy_input_focus/src/auto_directional_navigation.rs @@ -1,6 +1,6 @@ //! An optional but recommended automatic directional navigation system, powered by //! the [`AutoDirectionalNavigation`] component. -//! Prerequisites: Must have the `bevy_camera` and `bevy_ui` features enabled. +//! Prerequisites: Must have the `auto_nav` feature enabled. use alloc::vec::Vec; use bevy_camera::visibility::InheritedVisibility; @@ -39,7 +39,8 @@ use bevy_reflect::{prelude::*, Reflect}; /// **Workarounds** for multi-layer UIs: /// /// 1. **Per-layer manual edge generation**: Query entities by layer and call -/// [`auto_generate_navigation_edges()`] separately for each layer: +/// [`auto_generate_navigation_edges()`](crate::directional_navigation::auto_generate_navigation_edges) +/// separately for each layer: /// ```rust,ignore /// for layer in &layers { /// let nodes: Vec = query_layer(layer).collect(); @@ -47,7 +48,8 @@ use bevy_reflect::{prelude::*, Reflect}; /// } /// ``` /// -/// 2. **Manual cross-layer navigation**: Use [`DirectionalNavigationMap::add_edge()`] +/// 2. **Manual cross-layer navigation**: Use +/// [`DirectionalNavigationMap::add_edge()`](crate::directional_navigation::DirectionalNavigationMap::add_edge) /// to define explicit connections between layers (e.g., "Back" button to main menu). /// /// 3. **Remove component when layer is hidden**: Dynamically add/remove @@ -65,10 +67,10 @@ use bevy_reflect::{prelude::*, Reflect}; /// - **Dynamically toggle**: Remove/insert the component at runtime to enable/disable /// automatic navigation as needed. /// -/// Manual edges defined via [`DirectionalNavigationMap`] are completely independent and -/// will continue to work regardless of this component. +/// Manual edges defined via [`DirectionalNavigationMap`](crate::directional_navigation::DirectionalNavigationMap) +/// are completely independent and will continue to work regardless of this component. /// -/// # Requirements (for `bevy_ui`) +/// # Requirements for `bevy_ui` /// /// Entities must also have: /// - [`ComputedNode`] - for size information @@ -78,8 +80,9 @@ use bevy_reflect::{prelude::*, Reflect}; /// /// # Custom UI Systems /// -/// For custom UI frameworks, you can call [`auto_generate_navigation_edges`] directly -/// in your own system instead of using this component. +/// For custom UI frameworks, you can call +/// [`auto_generate_navigation_edges`](crate::directional_navigation::auto_generate_navigation_edges) +/// directly in your own system instead of using this component. #[derive(Component, Default, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index b4e2621793cef..1ffdc6fd14a7a 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -67,8 +67,17 @@ use bevy_ecs::{ system::SystemParam, }; use bevy_math::{CompassOctant, Vec2}; +#[cfg_attr( + feature = "auto_nav", + expect( + unused_imports, + reason = "PhantomData is not used if auto_nav is enabled" + ) +)] +use core::marker::PhantomData; use thiserror::Error; +#[cfg(feature = "auto_nav")] use crate::auto_directional_navigation::AutoDirectionalNavigator; use crate::{ navigator::{find_best_candidate, FocusableArea, NavigatorConfig}, @@ -263,8 +272,11 @@ pub struct DirectionalNavigation<'w, 's> { pub focus: ResMut<'w, InputFocus>, /// The directional navigation map containing manually defined connections between entities. pub map: Res<'w, DirectionalNavigationMap>, + #[cfg(feature = "auto_nav")] /// The system param that holds our automatic navigation system logic. pub(crate) auto_directional_navigator: AutoDirectionalNavigator<'w, 's>, + #[cfg(not(feature = "auto_nav"))] + marker: PhantomData<&'s ()> } impl<'w, 's> DirectionalNavigation<'w, 's> { @@ -285,6 +297,7 @@ impl<'w, 's> DirectionalNavigation<'w, 's> { return Ok(new_focus); } + #[cfg(feature = "auto_nav")] // If automatic navigation is enabled, try to get the result there as well. if let Some(new_focus) = self .auto_directional_navigator diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index c1ac1f2e8aec9..042b7a6304acb 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -24,6 +24,7 @@ extern crate alloc; mod navigator; pub use navigator::*; +#[cfg(feature = "auto_nav")] pub mod auto_directional_navigation; pub mod directional_navigation; pub mod tab_navigation; diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 989cddf728d56..36cee83b25941 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -432,6 +432,8 @@ keyboard = ["bevy_input/keyboard", "bevy_input_focus?/keyboard"] gamepad = ["bevy_input/gamepad", "bevy_input_focus?/gamepad"] touch = ["bevy_input/touch"] gestures = ["bevy_input/gestures"] +# Enables automatic navigation +auto_nav = ["bevy_input_focus?/auto_nav"] hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"] diff --git a/examples/ui/auto_directional_navigation.rs b/examples/ui/auto_directional_navigation.rs index a3139760cb62d..5e7c1f16217bd 100644 --- a/examples/ui/auto_directional_navigation.rs +++ b/examples/ui/auto_directional_navigation.rs @@ -1,4 +1,5 @@ //! Demonstrates automatic directional navigation with zero configuration. +//! You must have the auto_nav feature enabled //! //! Unlike the manual `directional_navigation` example, this shows how to use automatic //! navigation by simply adding the `AutoDirectionalNavigation` component to UI elements. From 759bc4f9a086a076191c5351369377956feeddb6 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Thu, 1 Jan 2026 03:07:18 -0500 Subject: [PATCH 03/15] style: lints! --- .../src/auto_directional_navigation.rs | 12 +++++----- .../src/directional_navigation.rs | 22 ++++--------------- examples/ui/auto_directional_navigation.rs | 2 +- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/crates/bevy_input_focus/src/auto_directional_navigation.rs b/crates/bevy_input_focus/src/auto_directional_navigation.rs index 02bbf813d9d64..3a73e5aa8c20f 100644 --- a/crates/bevy_input_focus/src/auto_directional_navigation.rs +++ b/crates/bevy_input_focus/src/auto_directional_navigation.rs @@ -20,7 +20,7 @@ use bevy_reflect::{prelude::*, Reflect}; /// /// ```rust /// # use bevy_ecs::prelude::*; -/// # use bevy_input_focus::directional_navigation::AutoDirectionalNavigation; +/// # use bevy_input_focus::auto_directional_navigation::AutoDirectionalNavigation; /// fn spawn_auto_nav_button(mut commands: Commands) { /// commands.spawn(( /// // ... Button, Node, etc. ... @@ -48,8 +48,8 @@ use bevy_reflect::{prelude::*, Reflect}; /// } /// ``` /// -/// 2. **Manual cross-layer navigation**: Use -/// [`DirectionalNavigationMap::add_edge()`](crate::directional_navigation::DirectionalNavigationMap::add_edge) +/// 2. **Manual cross-layer navigation**: Use +/// [`DirectionalNavigationMap::add_edge()`](crate::directional_navigation::DirectionalNavigationMap::add_edge) /// to define explicit connections between layers (e.g., "Back" button to main menu). /// /// 3. **Remove component when layer is hidden**: Dynamically add/remove @@ -67,7 +67,7 @@ use bevy_reflect::{prelude::*, Reflect}; /// - **Dynamically toggle**: Remove/insert the component at runtime to enable/disable /// automatic navigation as needed. /// -/// Manual edges defined via [`DirectionalNavigationMap`](crate::directional_navigation::DirectionalNavigationMap) +/// Manual edges defined via [`DirectionalNavigationMap`](crate::directional_navigation::DirectionalNavigationMap) /// are completely independent and will continue to work regardless of this component. /// /// # Requirements for `bevy_ui` @@ -80,8 +80,8 @@ use bevy_reflect::{prelude::*, Reflect}; /// /// # Custom UI Systems /// -/// For custom UI frameworks, you can call -/// [`auto_generate_navigation_edges`](crate::directional_navigation::auto_generate_navigation_edges) +/// For custom UI frameworks, you can call +/// [`auto_generate_navigation_edges`](crate::directional_navigation::auto_generate_navigation_edges) /// directly in your own system instead of using this component. #[derive(Component, Default, Debug, Clone, Copy, PartialEq)] #[cfg_attr( diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index 1ffdc6fd14a7a..412811f482744 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -23,22 +23,8 @@ //! //! The easiest way to set up navigation is to add the //! [`AutoDirectionalNavigation`](crate::auto_directional_navigation::AutoDirectionalNavigation) component -//! to your UI entities. You must have the `bevy_camera` and `bevy_ui` feature enabled to leverage this navigation. -//! The system will automatically compute the nearest neighbor in each direction -//! based on position and size: -//! -//! ```rust,no_run -//! # use bevy_ecs::prelude::*; -//! # use bevy_input_focus::directional_navigation::AutoDirectionalNavigation; -//! # use bevy_ui::Node; -//! fn spawn_button(mut commands: Commands) { -//! commands.spawn(( -//! Node::default(), -//! // ... other UI components ... -//! AutoDirectionalNavigation::default(), // That's it! -//! )); -//! } -//! ``` +//! to your UI entities. You must have the `auto_nav` feature enabled to leverage this navigation. +//! For proper usage, refer to [`AutoDirectionalNavigation`](crate::auto_directional_navigation::AutoDirectionalNavigation) //! //! ## Manual Navigation //! @@ -276,7 +262,7 @@ pub struct DirectionalNavigation<'w, 's> { /// The system param that holds our automatic navigation system logic. pub(crate) auto_directional_navigator: AutoDirectionalNavigator<'w, 's>, #[cfg(not(feature = "auto_nav"))] - marker: PhantomData<&'s ()> + marker: PhantomData<&'s ()>, } impl<'w, 's> DirectionalNavigation<'w, 's> { @@ -357,7 +343,7 @@ pub trait Navigable { /// # Example /// /// ```rust -/// # use bevy_input_focus::directional_navigation::*; +/// # use bevy_input_focus::{directional_navigation::*, FocusableArea, NavigatorConfig}; /// # use bevy_ecs::entity::Entity; /// # use bevy_math::Vec2; /// let mut nav_map = DirectionalNavigationMap::default(); diff --git a/examples/ui/auto_directional_navigation.rs b/examples/ui/auto_directional_navigation.rs index 5e7c1f16217bd..78a345aae77f3 100644 --- a/examples/ui/auto_directional_navigation.rs +++ b/examples/ui/auto_directional_navigation.rs @@ -1,5 +1,5 @@ //! Demonstrates automatic directional navigation with zero configuration. -//! You must have the auto_nav feature enabled +//! You must have the `auto_nav` feature enabled //! //! Unlike the manual `directional_navigation` example, this shows how to use automatic //! navigation by simply adding the `AutoDirectionalNavigation` component to UI elements. From 303369445b3739b9741d26ee9a4f7fb53a6f1e8a Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Thu, 1 Jan 2026 11:19:56 -0500 Subject: [PATCH 04/15] docs: adds new auto_natv feature to docs --- docs/cargo_features.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 231038b9e1f31..8916e9a151296 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -64,6 +64,7 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio |asset_processor|Enables the built-in asset processor for processed assets.| |async-io|Use async-io's implementation of block_on instead of futures-lite's implementation. This is preferred if your application uses async-io.| |async_executor|Uses `async-executor` as a task execution backend.| +|auto_nav|Enable the automatic navigation subsystem in bevy_input_focus| |basis-universal|Basis Universal compressed texture support| |bevy_animation|Provides animation functionality| |bevy_anti_alias|Provides various anti aliasing solutions| From 020600f89cff7b85cae412915b099045f4f82c92 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Thu, 1 Jan 2026 12:30:59 -0500 Subject: [PATCH 05/15] refactor: removes auto_nav feature, moves logic to bevy_ui --- Cargo.toml | 4 - crates/bevy_input_focus/Cargo.toml | 5 -- .../src/directional_navigation.rs | 60 ++++--------- crates/bevy_input_focus/src/lib.rs | 5 +- crates/bevy_input_focus/src/navigator.rs | 4 +- crates/bevy_internal/Cargo.toml | 2 - crates/bevy_ui/Cargo.toml | 1 + .../src/directional_navigation.rs} | 84 +++++++++++-------- crates/bevy_ui/src/lib.rs | 1 + examples/ui/auto_directional_navigation.rs | 11 ++- 10 files changed, 75 insertions(+), 102 deletions(-) rename crates/{bevy_input_focus/src/auto_directional_navigation.rs => bevy_ui/src/directional_navigation.rs} (69%) diff --git a/Cargo.toml b/Cargo.toml index 6481666c9f918..d5145a9347685 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -396,9 +396,6 @@ bevy_log = ["bevy_internal/bevy_log"] # Enable input focus subsystem bevy_input_focus = ["bevy_internal/bevy_input_focus"] -# Enable the automatic navigation subsystem in bevy_input_focus -auto_nav = ["bevy_internal/auto_nav"] - # Experimental headless widget collection for Bevy UI. experimental_bevy_ui_widgets = ["bevy_internal/bevy_ui_widgets"] @@ -5071,7 +5068,6 @@ name = "auto_directional_navigation" path = "examples/ui/auto_directional_navigation.rs" # Causes an ICE on docs.rs doc-scrape-examples = false -required-features = ["auto_nav"] [package.metadata.example.auto_directional_navigation] name = "Automatic Directional Navigation" diff --git a/crates/bevy_input_focus/Cargo.toml b/crates/bevy_input_focus/Cargo.toml index 9db3a81298ece..f14aa848753b3 100644 --- a/crates/bevy_input_focus/Cargo.toml +++ b/crates/bevy_input_focus/Cargo.toml @@ -42,9 +42,6 @@ serialize = [ "bevy_window/serialize", ] -## Adds auto navigation capability and enables its use -auto_nav = ["dep:bevy_camera", "dep:bevy_ui"] - # Platform Compatibility ## Allows access to the `std` crate. Enabling this feature will prevent compilation @@ -74,12 +71,10 @@ libm = ["bevy_math/libm", "bevy_window/libm"] [dependencies] # bevy bevy_app = { path = "../bevy_app", version = "0.18.0-dev", default-features = false } -bevy_camera = { path = "../bevy_camera", version = "0.18.0-dev", default-features = false, optional = true } bevy_ecs = { path = "../bevy_ecs", version = "0.18.0-dev", default-features = false } bevy_input = { path = "../bevy_input", version = "0.18.0-dev", default-features = false } bevy_math = { path = "../bevy_math", version = "0.18.0-dev", default-features = false } bevy_picking = { path = "../bevy_picking", version = "0.18.0-dev", default-features = false, optional = true } -bevy_ui = { path = "../bevy_ui", version = "0.18.0-dev", default-features = false, optional = true } bevy_window = { path = "../bevy_window", version = "0.18.0-dev", default-features = false } bevy_reflect = { path = "../bevy_reflect", version = "0.18.0-dev", features = [ "glam", diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index 412811f482744..47fb675409b13 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -21,10 +21,8 @@ //! //! ## Automatic Navigation (Recommended) //! -//! The easiest way to set up navigation is to add the -//! [`AutoDirectionalNavigation`](crate::auto_directional_navigation::AutoDirectionalNavigation) component -//! to your UI entities. You must have the `auto_nav` feature enabled to leverage this navigation. -//! For proper usage, refer to [`AutoDirectionalNavigation`](crate::auto_directional_navigation::AutoDirectionalNavigation) +//! The easiest way to set up navigation is to add the [`AutoDirectionalNavigation`] component to your UI entities. +//! This component is available in the `bevy_ui` crate. //! //! ## Manual Navigation //! @@ -46,6 +44,10 @@ //! - **Cross-layer navigation**: Connect elements across different UI layers or z-index levels //! - **Custom behavior**: Implement domain-specific navigation patterns (e.g., spreadsheet-style wrapping) +use crate::{ + navigator::{find_best_candidate, FocusableArea, NavigatorConfig}, + InputFocus, +}; use bevy_app::prelude::*; use bevy_ecs::{ entity::{EntityHashMap, EntityHashSet}, @@ -53,23 +55,8 @@ use bevy_ecs::{ system::SystemParam, }; use bevy_math::{CompassOctant, Vec2}; -#[cfg_attr( - feature = "auto_nav", - expect( - unused_imports, - reason = "PhantomData is not used if auto_nav is enabled" - ) -)] -use core::marker::PhantomData; use thiserror::Error; -#[cfg(feature = "auto_nav")] -use crate::auto_directional_navigation::AutoDirectionalNavigator; -use crate::{ - navigator::{find_best_candidate, FocusableArea, NavigatorConfig}, - InputFocus, -}; - #[cfg(feature = "bevy_reflect")] use bevy_reflect::{prelude::*, Reflect}; @@ -253,19 +240,14 @@ impl DirectionalNavigationMap { /// A system parameter for navigating between focusable entities in a directional way. #[derive(SystemParam, Debug)] -pub struct DirectionalNavigation<'w, 's> { +pub struct DirectionalNavigation<'w> { /// The currently focused entity. pub focus: ResMut<'w, InputFocus>, /// The directional navigation map containing manually defined connections between entities. pub map: Res<'w, DirectionalNavigationMap>, - #[cfg(feature = "auto_nav")] - /// The system param that holds our automatic navigation system logic. - pub(crate) auto_directional_navigator: AutoDirectionalNavigator<'w, 's>, - #[cfg(not(feature = "auto_nav"))] - marker: PhantomData<&'s ()>, } -impl<'w, 's> DirectionalNavigation<'w, 's> { +impl<'w> DirectionalNavigation<'w> { /// Navigates to the neighbor in a given direction from the current focus, if any. /// /// Returns the new focus if successful. @@ -280,26 +262,16 @@ impl<'w, 's> DirectionalNavigation<'w, 's> { // Respect manual edges first if let Some(new_focus) = self.map.get_neighbor(current_focus, direction) { self.focus.set(new_focus); - return Ok(new_focus); - } - - #[cfg(feature = "auto_nav")] - // If automatic navigation is enabled, try to get the result there as well. - if let Some(new_focus) = self - .auto_directional_navigator - .get_neighbor(current_focus, direction) - { - self.focus.set(new_focus); - return Ok(new_focus); + Ok(new_focus) + } else { + Err(DirectionalNavigationError::NoNeighborInDirection { + current_focus, + direction, + }) } - - return Err(DirectionalNavigationError::NoNeighborInDirection { - current_focus, - direction, - }); + } else { + Err(DirectionalNavigationError::NoFocus) } - - Err(DirectionalNavigationError::NoFocus) } } diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 042b7a6304acb..508fd92cbd7b3 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -22,11 +22,8 @@ extern crate std; extern crate alloc; -mod navigator; -pub use navigator::*; -#[cfg(feature = "auto_nav")] -pub mod auto_directional_navigation; pub mod directional_navigation; +pub mod navigator; pub mod tab_navigation; // This module is too small / specific to be exported by the crate, diff --git a/crates/bevy_input_focus/src/navigator.rs b/crates/bevy_input_focus/src/navigator.rs index 3a7d90bc8d7ba..f4e8750c5ddc9 100644 --- a/crates/bevy_input_focus/src/navigator.rs +++ b/crates/bevy_input_focus/src/navigator.rs @@ -1,4 +1,4 @@ -//! Common structs and functions used in both automatic navigators and manual navigators. +//! Common structs and functions that can be used to create navigation systems. use bevy_ecs::prelude::*; use bevy_math::{CompassOctant, Dir2, Rect, Vec2}; @@ -235,7 +235,7 @@ fn score_candidate( /// Finds the best entity to navigate to from the origin towards the given direction. /// /// For details on what "best" means here, refer to [`NavigatorConfig`]. -pub(crate) fn find_best_candidate( +pub fn find_best_candidate( origin: &FocusableArea, direction: CompassOctant, candidates: &[FocusableArea], diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 36cee83b25941..989cddf728d56 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -432,8 +432,6 @@ keyboard = ["bevy_input/keyboard", "bevy_input_focus?/keyboard"] gamepad = ["bevy_input/gamepad", "bevy_input_focus?/gamepad"] touch = ["bevy_input/touch"] gestures = ["bevy_input/gestures"] -# Enables automatic navigation -auto_nav = ["bevy_input_focus?/auto_nav"] hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"] diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index a3267fbee8716..155699b82a154 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -19,6 +19,7 @@ bevy_derive = { path = "../bevy_derive", version = "0.18.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.18.0-dev" } bevy_image = { path = "../bevy_image", version = "0.18.0-dev" } bevy_input = { path = "../bevy_input", version = "0.18.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", version = "0.18.0-dev" } bevy_math = { path = "../bevy_math", version = "0.18.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.18.0-dev" } bevy_sprite = { path = "../bevy_sprite", version = "0.18.0-dev", features = [ diff --git a/crates/bevy_input_focus/src/auto_directional_navigation.rs b/crates/bevy_ui/src/directional_navigation.rs similarity index 69% rename from crates/bevy_input_focus/src/auto_directional_navigation.rs rename to crates/bevy_ui/src/directional_navigation.rs index 3a73e5aa8c20f..07eb535fc6092 100644 --- a/crates/bevy_input_focus/src/auto_directional_navigation.rs +++ b/crates/bevy_ui/src/directional_navigation.rs @@ -1,16 +1,16 @@ //! An optional but recommended automatic directional navigation system, powered by //! the [`AutoDirectionalNavigation`] component. -//! Prerequisites: Must have the `auto_nav` feature enabled. -use alloc::vec::Vec; +use crate::{ComputedNode, ComputedUiTargetCamera, UiGlobalTransform}; use bevy_camera::visibility::InheritedVisibility; use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_math::CompassOctant; -use bevy_ui::{ComputedNode, ComputedUiTargetCamera, UiGlobalTransform}; -use crate::navigator::{find_best_candidate, FocusableArea, NavigatorConfig}; +use bevy_input_focus::{ + directional_navigation::{DirectionalNavigation, DirectionalNavigationError}, + navigator::*, +}; -#[cfg(feature = "bevy_reflect")] use bevy_reflect::{prelude::*, Reflect}; /// Marker component to enable automatic directional navigation to and from the entity. @@ -20,7 +20,7 @@ use bevy_reflect::{prelude::*, Reflect}; /// /// ```rust /// # use bevy_ecs::prelude::*; -/// # use bevy_input_focus::auto_directional_navigation::AutoDirectionalNavigation; +/// # use bevy_ui::directional_navigation::AutoDirectionalNavigation; /// fn spawn_auto_nav_button(mut commands: Commands) { /// commands.spawn(( /// // ... Button, Node, etc. ... @@ -39,7 +39,7 @@ use bevy_reflect::{prelude::*, Reflect}; /// **Workarounds** for multi-layer UIs: /// /// 1. **Per-layer manual edge generation**: Query entities by layer and call -/// [`auto_generate_navigation_edges()`](crate::directional_navigation::auto_generate_navigation_edges) +/// [`auto_generate_navigation_edges()`](bevy_input_focus::directional_navigation::auto_generate_navigation_edges) /// separately for each layer: /// ```rust,ignore /// for layer in &layers { @@ -49,11 +49,11 @@ use bevy_reflect::{prelude::*, Reflect}; /// ``` /// /// 2. **Manual cross-layer navigation**: Use -/// [`DirectionalNavigationMap::add_edge()`](crate::directional_navigation::DirectionalNavigationMap::add_edge) +/// [`DirectionalNavigationMap::add_edge()`](bevy_input_focus::directional_navigation::DirectionalNavigationMap::add_edge) /// to define explicit connections between layers (e.g., "Back" button to main menu). /// /// 3. **Remove component when layer is hidden**: Dynamically add/remove -/// `AutoDirectionalNavigation` based on which layers are currently active. +/// [`AutoDirectionalNavigation`] based on which layers are currently active. /// /// See issue [#21679](https://github.com/bevyengine/bevy/issues/21679) for planned /// improvements to layer-aware automatic navigation. @@ -62,15 +62,15 @@ use bevy_reflect::{prelude::*, Reflect}; /// /// To disable automatic navigation for specific entities: /// -/// - **Remove the component**: Simply don't add `AutoDirectionalNavigation` to entities +/// - **Remove the component**: Simply don't add [`AutoDirectionalNavigation`] to entities /// that should only use manual navigation edges. /// - **Dynamically toggle**: Remove/insert the component at runtime to enable/disable /// automatic navigation as needed. /// -/// Manual edges defined via [`DirectionalNavigationMap`](crate::directional_navigation::DirectionalNavigationMap) +/// Manual edges defined via [`DirectionalNavigationMap`](bevy_input_focus::directional_navigation::DirectionalNavigationMap) /// are completely independent and will continue to work regardless of this component. /// -/// # Requirements for `bevy_ui` +/// # Additional Requirements /// /// Entities must also have: /// - [`ComputedNode`] - for size information @@ -81,24 +81,26 @@ use bevy_reflect::{prelude::*, Reflect}; /// # Custom UI Systems /// /// For custom UI frameworks, you can call -/// [`auto_generate_navigation_edges`](crate::directional_navigation::auto_generate_navigation_edges) +/// [`auto_generate_navigation_edges`](bevy_input_focus::directional_navigation::auto_generate_navigation_edges) /// directly in your own system instead of using this component. -#[derive(Component, Default, Debug, Clone, Copy, PartialEq)] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Component, Default, Debug, PartialEq, Clone) -)] +#[derive(Component, Default, Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(Component, Default, Debug, PartialEq, Clone)] pub struct AutoDirectionalNavigation { /// Whether to also consider `TabIndex` for navigation order hints. /// Currently unused but reserved for future functionality. pub respect_tab_order: bool, } -/// A system parameter for auto navigating between focusable entities in a directional way. +/// A system parameter for combining manual and auto navigation between focusable entities in a directional way. +/// This wraps the [`DirectionalNavigation`] system parameter provided by `bevy_input_focus` and +/// augments it with auto directional navigation. +/// To use, the [`DirectionalNavigationPlugin`](bevy_input_focus::directional_navigation::DirectionalNavigationPlugin) +/// must be added to the app. #[derive(SystemParam, Debug)] -pub(crate) struct AutoDirectionalNavigator<'w, 's> { - /// Configuration for the automatic navigation system +pub struct AutoDirectionalNavigator<'w, 's> { + /// A system parameter for the manual directional navigation system provided by `bevy_input_focus` + pub manual_directional_navigation: DirectionalNavigation<'w>, + /// Configuration for the automated portion of the navigation algorithm. pub config: Res<'w, NavigatorConfig>, /// The entities which can possibly be navigated to automatically. navigable_entities_query: Query< @@ -132,22 +134,34 @@ impl<'w, 's> AutoDirectionalNavigator<'w, 's> { /// /// Returns a neighbor if successful. /// Returns None if there is no neighbor in the requested direction. - pub fn get_neighbor( + pub fn navigate( &mut self, - from_entity: Entity, direction: CompassOctant, - ) -> Option { - if let Some((target_camera, origin)) = self.entity_to_camera_and_focusable_area(from_entity) - && let Some(new_focus) = find_best_candidate( - &origin, - direction, - &self.get_navigable_nodes(target_camera), - &self.config, - ) - { - Some(new_focus) + ) -> Result { + if let Some(current_focus) = self.manual_directional_navigation.focus.0 { + // Respect manual edges first + if let Ok(new_focus) = self.manual_directional_navigation.navigate(direction) { + self.manual_directional_navigation.focus.set(new_focus); + Ok(new_focus) + } else if let Some((target_camera, origin)) = + self.entity_to_camera_and_focusable_area(current_focus) + && let Some(new_focus) = find_best_candidate( + &origin, + direction, + &self.get_navigable_nodes(target_camera), + &self.config, + ) + { + self.manual_directional_navigation.focus.set(new_focus); + Ok(new_focus) + } else { + Err(DirectionalNavigationError::NoNeighborInDirection { + current_focus, + direction, + }) + } } else { - None + Err(DirectionalNavigationError::NoFocus) } } diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 4b698a3de4a0c..11c120df45bda 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -10,6 +10,7 @@ //! Spawn UI elements with [`widget::Button`], [`ImageNode`](widget::ImageNode), [`Text`](prelude::Text) and [`Node`] //! This UI is laid out with the Flexbox and CSS Grid layout models (see ) +pub mod directional_navigation; pub mod interaction_states; pub mod measurement; pub mod update; diff --git a/examples/ui/auto_directional_navigation.rs b/examples/ui/auto_directional_navigation.rs index 78a345aae77f3..c4a59ac1a6f50 100644 --- a/examples/ui/auto_directional_navigation.rs +++ b/examples/ui/auto_directional_navigation.rs @@ -1,5 +1,4 @@ //! Demonstrates automatic directional navigation with zero configuration. -//! You must have the `auto_nav` feature enabled //! //! Unlike the manual `directional_navigation` example, this shows how to use automatic //! navigation by simply adding the `AutoDirectionalNavigation` component to UI elements. @@ -18,9 +17,8 @@ use core::time::Duration; use bevy::{ camera::NormalizedRenderTarget, input_focus::{ - auto_directional_navigation::AutoDirectionalNavigation, - directional_navigation::{DirectionalNavigation, DirectionalNavigationPlugin}, - InputDispatchPlugin, InputFocus, InputFocusVisible, NavigatorConfig, + directional_navigation::DirectionalNavigationPlugin, + InputDispatchPlugin, InputFocus, InputFocusVisible, navigator::NavigatorConfig, }, math::{CompassOctant, Dir2}, picking::{ @@ -29,6 +27,7 @@ use bevy::{ }, platform::collections::HashSet, prelude::*, + ui::directional_navigation::{AutoDirectionalNavigation, AutoDirectionalNavigator}, }; fn main() { @@ -323,7 +322,7 @@ fn process_inputs( } } -fn navigate(action_state: Res, mut directional_navigation: DirectionalNavigation) { +fn navigate(action_state: Res, mut auto_directional_navigator: AutoDirectionalNavigator) { let net_east_west = action_state .pressed_actions .contains(&DirectionalNavigationAction::Right) as i8 @@ -344,7 +343,7 @@ fn navigate(action_state: Res, mut directional_navigation: Directio .map(CompassOctant::from); if let Some(direction) = maybe_direction { - match directional_navigation.navigate(direction) { + match auto_directional_navigator.navigate(direction) { Ok(_entity) => { // Successfully navigated } From 541130deb1828fb3dbe4e4e704249a83cd6ad5d1 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Thu, 1 Jan 2026 12:41:30 -0500 Subject: [PATCH 06/15] docs: update old release notes, features --- docs/cargo_features.md | 1 - .../automatic_directional_navigation.md | 109 ++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 release-content/release-notes/automatic_directional_navigation.md diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 8916e9a151296..231038b9e1f31 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -64,7 +64,6 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio |asset_processor|Enables the built-in asset processor for processed assets.| |async-io|Use async-io's implementation of block_on instead of futures-lite's implementation. This is preferred if your application uses async-io.| |async_executor|Uses `async-executor` as a task execution backend.| -|auto_nav|Enable the automatic navigation subsystem in bevy_input_focus| |basis-universal|Basis Universal compressed texture support| |bevy_animation|Provides animation functionality| |bevy_anti_alias|Provides various anti aliasing solutions| diff --git a/release-content/release-notes/automatic_directional_navigation.md b/release-content/release-notes/automatic_directional_navigation.md new file mode 100644 index 0000000000000..15b7f518e59e7 --- /dev/null +++ b/release-content/release-notes/automatic_directional_navigation.md @@ -0,0 +1,109 @@ +--- +title: Automatic Directional Navigation +authors: ["@jbuehler23"] +pull_requests: [21668] +--- + +Bevy now supports **automatic directional navigation graph generation** for UI elements! No more tedious manual wiring of navigation connections for your menus and UI screens. + +## What's New? + +Previously, creating directional navigation for UI required manually defining every connection between focusable elements using `DirectionalNavigationMap`. For dynamic UIs or complex layouts, this was time-consuming and error-prone. + +Now, you can simply add the `AutoDirectionalNavigation` component to your UI entities, and Bevy will automatically compute navigation connections based on spatial positioning. The system intelligently finds the nearest neighbor in each of the 8 compass directions (North, Northeast, East, etc.), considering: + +- **Distance**: Closer elements are preferred +- **Alignment**: Elements that are more directly in line with the navigation direction are favored +- **Overlap**: For cardinal directions (N/S/E/W), the system ensures sufficient perpendicular overlap + +## How to Use It + +Simply add the `AutoDirectionalNavigation` component to your UI entities: + +```rust +commands.spawn(( + Button, + Node { /* ... */ }, + AutoDirectionalNavigation::default(), + // ... other components +)); +``` + +And use the new `AutoDirectionalNavigator` system parameter instead of `DirectionalNavigation`. + +That's it! The navigator will consider any entities with the `AutoDirectionalNavigation` component when navigating. + +### Configuration + +You can tune the behavior using the `NavigatorConfig` resource: + +```rust +app.insert_resource(NavigatorConfig { + // Minimum overlap required (0.0 = any overlap, 1.0 = perfect alignment) + min_alignment_factor: 0.0, + // Optional maximum distance for connections + max_search_distance: Some(500.0), + // Whether to strongly prefer well-aligned nodes + prefer_aligned: true, +}); +``` + +### Manual Override + +Automatic navigation respects manually-defined edges. If you want to override specific connections, you can still use `DirectionalNavigationMap::add_edge()` or `add_symmetrical_edge()`, and those connections will take precedence over the auto-generated ones. +You may also call `auto_generate_navigation_edges()` directly, if you have multiple UI layers (though may not be widely used) + +## Why This Matters + +This feature dramatically simplifies UI navigation setup: + +- **Less boilerplate**: No need to manually wire up dozens or hundreds of navigation connections +- **Works with dynamic UIs**: Automatically adapts when UI elements are added, removed, or repositioned +- **Flexible**: Mix automatic and manual navigation as needed +- **Configurable**: Tune the algorithm to match your UI's needs + +Whether you're building menus, inventory screens, or any other gamepad/keyboard-navigable UI, automatic directional navigation makes it much easier to create intuitive, responsive navigation experiences. + +## Migration Guide + +This is a non-breaking change. Existing manual navigation setups continue to work as before. + +If you want to convert existing manual navigation to automatic: + +**Before:** + +```rust +// Manually define all edges +directional_nav_map.add_looping_edges(&row_entities, CompassOctant::East); +directional_nav_map.add_edges(&column_entities, CompassOctant::South); +// ... repeat for all rows and columns +``` + +```rust +// Use the DirectionalNavigation SystemParam to navigate in your system +fn navigation_system(mut directional_navigation: DirectionalNavigation) { + // ... + directional_navigation.navigate(CompassOctant::East); + // ... +``` + +**After:** + +```rust +// Just add the component to your UI entities +commands.spawn(( + Button, + Node { /* ... */ }, + AutoDirectionalNavigation::default(), +)); +``` + +```rust +// Use the AutoDirectionalNavigator SystemParam to navigate +fn navigation_system(mut auto_directional_navigator: AutoDirectionalNavigator) { + // ... + auto_directional_navigator.navigate(CompassOctant::East); + // ... +``` + +Note: The automatic navigation system requires entities to have position and size information (`ComputedNode` and `UiGlobalTransform` for `bevy_ui` entities). From 6b9cad1da91a800d5e7828956ccc936c636aa88b Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Thu, 1 Jan 2026 12:57:26 -0500 Subject: [PATCH 07/15] docs: cleanups small stuff --- crates/bevy_input_focus/src/directional_navigation.rs | 6 ++++-- crates/bevy_input_focus/src/lib.rs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index 47fb675409b13..7ab5279a6521d 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -21,8 +21,10 @@ //! //! ## Automatic Navigation (Recommended) //! -//! The easiest way to set up navigation is to add the [`AutoDirectionalNavigation`] component to your UI entities. -//! This component is available in the `bevy_ui` crate. +//! The easiest way to set up navigation is to add the [`AutoDirectionalNavigation`] component +//! to your UI entities. This component is available in the `bevy_ui` crate. If you choose to +//! include automatic navigation, you should also use the [`AutoDirectionalNavigator`] system parameter +//! in that crate instead of [`DirectionalNavigation`]. //! //! ## Manual Navigation //! diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 508fd92cbd7b3..b9b8b17c546d3 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -22,8 +22,8 @@ extern crate std; extern crate alloc; -pub mod directional_navigation; pub mod navigator; +pub mod directional_navigation; pub mod tab_navigation; // This module is too small / specific to be exported by the crate, From 4f5b94a2af2682a8934427205ffbcb89def95710 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Thu, 1 Jan 2026 12:58:34 -0500 Subject: [PATCH 08/15] style: fmts, closes function in release notes --- crates/bevy_input_focus/src/directional_navigation.rs | 6 +++--- crates/bevy_input_focus/src/lib.rs | 2 +- crates/bevy_ui/src/directional_navigation.rs | 2 +- examples/ui/auto_directional_navigation.rs | 9 ++++++--- .../release-notes/automatic_directional_navigation.md | 2 ++ 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index 7ab5279a6521d..3c2b64dd48f99 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -21,9 +21,9 @@ //! //! ## Automatic Navigation (Recommended) //! -//! The easiest way to set up navigation is to add the [`AutoDirectionalNavigation`] component -//! to your UI entities. This component is available in the `bevy_ui` crate. If you choose to -//! include automatic navigation, you should also use the [`AutoDirectionalNavigator`] system parameter +//! The easiest way to set up navigation is to add the [`AutoDirectionalNavigation`] component +//! to your UI entities. This component is available in the `bevy_ui` crate. If you choose to +//! include automatic navigation, you should also use the [`AutoDirectionalNavigator`] system parameter //! in that crate instead of [`DirectionalNavigation`]. //! //! ## Manual Navigation diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index b9b8b17c546d3..508fd92cbd7b3 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -22,8 +22,8 @@ extern crate std; extern crate alloc; -pub mod navigator; pub mod directional_navigation; +pub mod navigator; pub mod tab_navigation; // This module is too small / specific to be exported by the crate, diff --git a/crates/bevy_ui/src/directional_navigation.rs b/crates/bevy_ui/src/directional_navigation.rs index 07eb535fc6092..a1f88b7e0b1f2 100644 --- a/crates/bevy_ui/src/directional_navigation.rs +++ b/crates/bevy_ui/src/directional_navigation.rs @@ -92,7 +92,7 @@ pub struct AutoDirectionalNavigation { } /// A system parameter for combining manual and auto navigation between focusable entities in a directional way. -/// This wraps the [`DirectionalNavigation`] system parameter provided by `bevy_input_focus` and +/// This wraps the [`DirectionalNavigation`] system parameter provided by `bevy_input_focus` and /// augments it with auto directional navigation. /// To use, the [`DirectionalNavigationPlugin`](bevy_input_focus::directional_navigation::DirectionalNavigationPlugin) /// must be added to the app. diff --git a/examples/ui/auto_directional_navigation.rs b/examples/ui/auto_directional_navigation.rs index c4a59ac1a6f50..6e4a127f0b64a 100644 --- a/examples/ui/auto_directional_navigation.rs +++ b/examples/ui/auto_directional_navigation.rs @@ -17,8 +17,8 @@ use core::time::Duration; use bevy::{ camera::NormalizedRenderTarget, input_focus::{ - directional_navigation::DirectionalNavigationPlugin, - InputDispatchPlugin, InputFocus, InputFocusVisible, navigator::NavigatorConfig, + directional_navigation::DirectionalNavigationPlugin, navigator::NavigatorConfig, + InputDispatchPlugin, InputFocus, InputFocusVisible, }, math::{CompassOctant, Dir2}, picking::{ @@ -322,7 +322,10 @@ fn process_inputs( } } -fn navigate(action_state: Res, mut auto_directional_navigator: AutoDirectionalNavigator) { +fn navigate( + action_state: Res, + mut auto_directional_navigator: AutoDirectionalNavigator, +) { let net_east_west = action_state .pressed_actions .contains(&DirectionalNavigationAction::Right) as i8 diff --git a/release-content/release-notes/automatic_directional_navigation.md b/release-content/release-notes/automatic_directional_navigation.md index 15b7f518e59e7..813e3df51dd59 100644 --- a/release-content/release-notes/automatic_directional_navigation.md +++ b/release-content/release-notes/automatic_directional_navigation.md @@ -85,6 +85,7 @@ fn navigation_system(mut directional_navigation: DirectionalNavigation) { // ... directional_navigation.navigate(CompassOctant::East); // ... +} ``` **After:** @@ -104,6 +105,7 @@ fn navigation_system(mut auto_directional_navigator: AutoDirectionalNavigator) { // ... auto_directional_navigator.navigate(CompassOctant::East); // ... +} ``` Note: The automatic navigation system requires entities to have position and size information (`ComputedNode` and `UiGlobalTransform` for `bevy_ui` entities). From 9116886dd84712d607d56cce808fcfa1476d16ff Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Thu, 1 Jan 2026 13:12:56 -0500 Subject: [PATCH 09/15] docs: updates file comment for dir_nav in bevy_ui --- crates/bevy_ui/src/directional_navigation.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/directional_navigation.rs b/crates/bevy_ui/src/directional_navigation.rs index a1f88b7e0b1f2..75420dd630a2b 100644 --- a/crates/bevy_ui/src/directional_navigation.rs +++ b/crates/bevy_ui/src/directional_navigation.rs @@ -1,5 +1,7 @@ -//! An optional but recommended automatic directional navigation system, powered by -//! the [`AutoDirectionalNavigation`] component. +//! An automatic directional navigation system, powered by the [`AutoDirectionalNavigation`] component. +//! +//! [`AutoDirectionalNavigator`] expands on the manual directional navigation system +//! provided by the [`DirectionalNavigation`] system parameter from `bevy_input_focus`. use crate::{ComputedNode, ComputedUiTargetCamera, UiGlobalTransform}; use bevy_camera::visibility::InheritedVisibility; From e96d12013202bfdbc0951d3a632097c5d49fc223 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Thu, 1 Jan 2026 13:34:28 -0500 Subject: [PATCH 10/15] docs: fixes doc --- crates/bevy_input_focus/src/directional_navigation.rs | 6 +++--- crates/bevy_input_focus/src/navigator.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index 3c2b64dd48f99..977fde9fe1741 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -21,9 +21,9 @@ //! //! ## Automatic Navigation (Recommended) //! -//! The easiest way to set up navigation is to add the [`AutoDirectionalNavigation`] component +//! The easiest way to set up navigation is to add the `AutoDirectionalNavigation` component //! to your UI entities. This component is available in the `bevy_ui` crate. If you choose to -//! include automatic navigation, you should also use the [`AutoDirectionalNavigator`] system parameter +//! include automatic navigation, you should also use the `AutoDirectionalNavigator` system parameter //! in that crate instead of [`DirectionalNavigation`]. //! //! ## Manual Navigation @@ -317,7 +317,7 @@ pub trait Navigable { /// # Example /// /// ```rust -/// # use bevy_input_focus::{directional_navigation::*, FocusableArea, NavigatorConfig}; +/// # use bevy_input_focus::{directional_navigation::*, navigator::{FocusableArea, NavigatorConfig}}; /// # use bevy_ecs::entity::Entity; /// # use bevy_math::Vec2; /// let mut nav_map = DirectionalNavigationMap::default(); diff --git a/crates/bevy_input_focus/src/navigator.rs b/crates/bevy_input_focus/src/navigator.rs index f4e8750c5ddc9..5fdc1cc346fd8 100644 --- a/crates/bevy_input_focus/src/navigator.rs +++ b/crates/bevy_input_focus/src/navigator.rs @@ -82,7 +82,7 @@ impl Default for NavigatorConfig { /// This struct represents a UI element used during directional navigation, /// containing its entity ID, center position, and size for spatial navigation calculations. /// -/// The term "focusable area" avoids confusion with UI [`Node`](bevy_ui::Node) components. +/// The term "focusable area" avoids confusion with UI `Node` components in `bevy_ui`. #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", From 47739d751a41ba2b037faed9be903a6e6884f81f Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Thu, 1 Jan 2026 16:50:55 -0500 Subject: [PATCH 11/15] docs: removes already removed release notes --- .../automatic_directional_navigation.md | 111 ------------------ 1 file changed, 111 deletions(-) delete mode 100644 release-content/release-notes/automatic_directional_navigation.md diff --git a/release-content/release-notes/automatic_directional_navigation.md b/release-content/release-notes/automatic_directional_navigation.md deleted file mode 100644 index 813e3df51dd59..0000000000000 --- a/release-content/release-notes/automatic_directional_navigation.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: Automatic Directional Navigation -authors: ["@jbuehler23"] -pull_requests: [21668] ---- - -Bevy now supports **automatic directional navigation graph generation** for UI elements! No more tedious manual wiring of navigation connections for your menus and UI screens. - -## What's New? - -Previously, creating directional navigation for UI required manually defining every connection between focusable elements using `DirectionalNavigationMap`. For dynamic UIs or complex layouts, this was time-consuming and error-prone. - -Now, you can simply add the `AutoDirectionalNavigation` component to your UI entities, and Bevy will automatically compute navigation connections based on spatial positioning. The system intelligently finds the nearest neighbor in each of the 8 compass directions (North, Northeast, East, etc.), considering: - -- **Distance**: Closer elements are preferred -- **Alignment**: Elements that are more directly in line with the navigation direction are favored -- **Overlap**: For cardinal directions (N/S/E/W), the system ensures sufficient perpendicular overlap - -## How to Use It - -Simply add the `AutoDirectionalNavigation` component to your UI entities: - -```rust -commands.spawn(( - Button, - Node { /* ... */ }, - AutoDirectionalNavigation::default(), - // ... other components -)); -``` - -And use the new `AutoDirectionalNavigator` system parameter instead of `DirectionalNavigation`. - -That's it! The navigator will consider any entities with the `AutoDirectionalNavigation` component when navigating. - -### Configuration - -You can tune the behavior using the `NavigatorConfig` resource: - -```rust -app.insert_resource(NavigatorConfig { - // Minimum overlap required (0.0 = any overlap, 1.0 = perfect alignment) - min_alignment_factor: 0.0, - // Optional maximum distance for connections - max_search_distance: Some(500.0), - // Whether to strongly prefer well-aligned nodes - prefer_aligned: true, -}); -``` - -### Manual Override - -Automatic navigation respects manually-defined edges. If you want to override specific connections, you can still use `DirectionalNavigationMap::add_edge()` or `add_symmetrical_edge()`, and those connections will take precedence over the auto-generated ones. -You may also call `auto_generate_navigation_edges()` directly, if you have multiple UI layers (though may not be widely used) - -## Why This Matters - -This feature dramatically simplifies UI navigation setup: - -- **Less boilerplate**: No need to manually wire up dozens or hundreds of navigation connections -- **Works with dynamic UIs**: Automatically adapts when UI elements are added, removed, or repositioned -- **Flexible**: Mix automatic and manual navigation as needed -- **Configurable**: Tune the algorithm to match your UI's needs - -Whether you're building menus, inventory screens, or any other gamepad/keyboard-navigable UI, automatic directional navigation makes it much easier to create intuitive, responsive navigation experiences. - -## Migration Guide - -This is a non-breaking change. Existing manual navigation setups continue to work as before. - -If you want to convert existing manual navigation to automatic: - -**Before:** - -```rust -// Manually define all edges -directional_nav_map.add_looping_edges(&row_entities, CompassOctant::East); -directional_nav_map.add_edges(&column_entities, CompassOctant::South); -// ... repeat for all rows and columns -``` - -```rust -// Use the DirectionalNavigation SystemParam to navigate in your system -fn navigation_system(mut directional_navigation: DirectionalNavigation) { - // ... - directional_navigation.navigate(CompassOctant::East); - // ... -} -``` - -**After:** - -```rust -// Just add the component to your UI entities -commands.spawn(( - Button, - Node { /* ... */ }, - AutoDirectionalNavigation::default(), -)); -``` - -```rust -// Use the AutoDirectionalNavigator SystemParam to navigate -fn navigation_system(mut auto_directional_navigator: AutoDirectionalNavigator) { - // ... - auto_directional_navigator.navigate(CompassOctant::East); - // ... -} -``` - -Note: The automatic navigation system requires entities to have position and size information (`ComputedNode` and `UiGlobalTransform` for `bevy_ui` entities). From 6aa97892b7154e1fd5860ee7edb68340fcb9916e Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Fri, 2 Jan 2026 22:18:09 -0500 Subject: [PATCH 12/15] refactor: undoes rename back to AutoNavigationConfig --- .../src/directional_navigation.rs | 18 +++++++++--------- crates/bevy_input_focus/src/navigator.rs | 12 ++++++------ crates/bevy_ui/src/directional_navigation.rs | 2 +- examples/ui/auto_directional_navigation.rs | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index 977fde9fe1741..79313f7078168 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -47,7 +47,7 @@ //! - **Custom behavior**: Implement domain-specific navigation patterns (e.g., spreadsheet-style wrapping) use crate::{ - navigator::{find_best_candidate, FocusableArea, NavigatorConfig}, + navigator::{find_best_candidate, FocusableArea, AutoNavigationConfig}, InputFocus, }; use bevy_app::prelude::*; @@ -69,7 +69,7 @@ pub struct DirectionalNavigationPlugin; impl Plugin for DirectionalNavigationPlugin { fn build(&self, app: &mut App) { app.init_resource::() - .init_resource::(); + .init_resource::(); } } @@ -317,11 +317,11 @@ pub trait Navigable { /// # Example /// /// ```rust -/// # use bevy_input_focus::{directional_navigation::*, navigator::{FocusableArea, NavigatorConfig}}; +/// # use bevy_input_focus::{directional_navigation::*, navigator::{FocusableArea, AutoNavigationConfig}}; /// # use bevy_ecs::entity::Entity; /// # use bevy_math::Vec2; /// let mut nav_map = DirectionalNavigationMap::default(); -/// let config = NavigatorConfig::default(); +/// let config = AutoNavigationConfig::default(); /// /// let nodes = vec![ /// FocusableArea { entity: Entity::PLACEHOLDER, position: Vec2::new(100.0, 100.0), size: Vec2::new(50.0, 50.0) }, @@ -333,7 +333,7 @@ pub trait Navigable { pub fn auto_generate_navigation_edges( nav_map: &mut DirectionalNavigationMap, nodes: &[FocusableArea], - config: &NavigatorConfig, + config: &AutoNavigationConfig, ) { // For each node, find best neighbor in each direction for origin in nodes { @@ -520,7 +520,7 @@ mod tests { focus.set(a); world.insert_resource(focus); - let config = NavigatorConfig::default(); + let config = AutoNavigationConfig::default(); world.insert_resource(config); assert_eq!(world.resource::().get(), Some(a)); @@ -542,7 +542,7 @@ mod tests { #[test] fn test_auto_generate_navigation_edges() { let mut nav_map = DirectionalNavigationMap::default(); - let config = NavigatorConfig::default(); + let config = AutoNavigationConfig::default(); // Create a 2x2 grid of nodes (using UI coordinates: smaller Y = higher on screen) let node_a = Entity::from_bits(1); // Top-left @@ -605,7 +605,7 @@ mod tests { #[test] fn test_auto_generate_respects_manual_edges() { let mut nav_map = DirectionalNavigationMap::default(); - let config = NavigatorConfig::default(); + let config = AutoNavigationConfig::default(); let node_a = Entity::from_bits(1); let node_b = Entity::from_bits(2); @@ -644,7 +644,7 @@ mod tests { #[test] fn test_edge_distance_vs_center_distance() { let mut nav_map = DirectionalNavigationMap::default(); - let config = NavigatorConfig::default(); + let config = AutoNavigationConfig::default(); let left = Entity::from_bits(1); let wide_top = Entity::from_bits(2); diff --git a/crates/bevy_input_focus/src/navigator.rs b/crates/bevy_input_focus/src/navigator.rs index 5fdc1cc346fd8..ceee53fa22876 100644 --- a/crates/bevy_input_focus/src/navigator.rs +++ b/crates/bevy_input_focus/src/navigator.rs @@ -16,7 +16,7 @@ use bevy_reflect::Reflect; derive(Reflect), reflect(Resource, Debug, PartialEq, Clone) )] -pub struct NavigatorConfig { +pub struct AutoNavigationConfig { /// Minimum overlap ratio (0.0-1.0) required along the perpendicular axis for cardinal directions. /// /// This parameter controls how much two UI elements must overlap in the perpendicular direction @@ -67,7 +67,7 @@ pub struct NavigatorConfig { pub prefer_aligned: bool, } -impl Default for NavigatorConfig { +impl Default for AutoNavigationConfig { fn default() -> Self { Self { min_alignment_factor: 0.0, // Any overlap is acceptable @@ -168,7 +168,7 @@ fn score_candidate( candidate_pos: Vec2, candidate_size: Vec2, octant: CompassOctant, - config: &NavigatorConfig, + config: &AutoNavigationConfig, ) -> f32 { // Get direction in mathematical coordinates, then flip Y for UI coordinates let dir = Dir2::from(octant).as_vec2() * Vec2::new(1.0, -1.0); @@ -234,12 +234,12 @@ fn score_candidate( /// Finds the best entity to navigate to from the origin towards the given direction. /// -/// For details on what "best" means here, refer to [`NavigatorConfig`]. +/// For details on what "best" means here, refer to [`AutoNavigationConfig`]. pub fn find_best_candidate( origin: &FocusableArea, direction: CompassOctant, candidates: &[FocusableArea], - config: &NavigatorConfig, + config: &AutoNavigationConfig, ) -> Option { // Find best candidate in this direction let mut best_candidate = None; @@ -341,7 +341,7 @@ mod tests { #[test] fn test_score_candidate() { - let config = NavigatorConfig::default(); + let config = AutoNavigationConfig::default(); let origin_pos = Vec2::new(100.0, 100.0); let origin_size = Vec2::new(50.0, 50.0); diff --git a/crates/bevy_ui/src/directional_navigation.rs b/crates/bevy_ui/src/directional_navigation.rs index 75420dd630a2b..ace12e3a9e4cd 100644 --- a/crates/bevy_ui/src/directional_navigation.rs +++ b/crates/bevy_ui/src/directional_navigation.rs @@ -103,7 +103,7 @@ pub struct AutoDirectionalNavigator<'w, 's> { /// A system parameter for the manual directional navigation system provided by `bevy_input_focus` pub manual_directional_navigation: DirectionalNavigation<'w>, /// Configuration for the automated portion of the navigation algorithm. - pub config: Res<'w, NavigatorConfig>, + pub config: Res<'w, AutoNavigationConfig>, /// The entities which can possibly be navigated to automatically. navigable_entities_query: Query< 'w, diff --git a/examples/ui/auto_directional_navigation.rs b/examples/ui/auto_directional_navigation.rs index 6e4a127f0b64a..2fafcc5240222 100644 --- a/examples/ui/auto_directional_navigation.rs +++ b/examples/ui/auto_directional_navigation.rs @@ -17,7 +17,7 @@ use core::time::Duration; use bevy::{ camera::NormalizedRenderTarget, input_focus::{ - directional_navigation::DirectionalNavigationPlugin, navigator::NavigatorConfig, + directional_navigation::DirectionalNavigationPlugin, navigator::AutoNavigationConfig, InputDispatchPlugin, InputFocus, InputFocusVisible, }, math::{CompassOctant, Dir2}, @@ -42,7 +42,7 @@ fn main() { // It starts as false, but we set it to true here as we would like to see the focus indicator .insert_resource(InputFocusVisible(true)) // Configure auto-navigation behavior - .insert_resource(NavigatorConfig { + .insert_resource(AutoNavigationConfig { // Require at least 10% overlap in perpendicular axis for cardinal directions min_alignment_factor: 0.1, // Don't connect nodes more than 500 pixels apart From 6077490da6c5a69395b219801c1ba7abc2e4d5f7 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Fri, 2 Jan 2026 22:31:52 -0500 Subject: [PATCH 13/15] refactor: puts FocusableArea and config back to where they were --- .../src/directional_navigation.rs | 99 ++++++++++++++++- crates/bevy_input_focus/src/navigator.rs | 102 +----------------- crates/bevy_ui/src/directional_navigation.rs | 6 +- examples/ui/auto_directional_navigation.rs | 2 +- 4 files changed, 103 insertions(+), 106 deletions(-) diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index 79313f7078168..247adc27ea238 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -46,10 +46,7 @@ //! - **Cross-layer navigation**: Connect elements across different UI layers or z-index levels //! - **Custom behavior**: Implement domain-specific navigation patterns (e.g., spreadsheet-style wrapping) -use crate::{ - navigator::{find_best_candidate, FocusableArea, AutoNavigationConfig}, - InputFocus, -}; +use crate::{navigator::find_best_candidate, InputFocus}; use bevy_app::prelude::*; use bevy_ecs::{ entity::{EntityHashMap, EntityHashSet}, @@ -73,6 +70,77 @@ impl Plugin for DirectionalNavigationPlugin { } } +/// Configuration resource for automatic directional navigation and for generating manual +/// navigation edges via [`auto_generate_navigation_edges`](crate::directional_navigation::auto_generate_navigation_edges) +/// +/// This resource controls how nodes should be automatically connected in each direction. +#[derive(Resource, Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Resource, Debug, PartialEq, Clone) +)] +pub struct AutoNavigationConfig { + /// Minimum overlap ratio (0.0-1.0) required along the perpendicular axis for cardinal directions. + /// + /// This parameter controls how much two UI elements must overlap in the perpendicular direction + /// to be considered reachable neighbors. It only applies to cardinal directions (`North`, `South`, `East`, `West`); + /// diagonal directions (`NorthEast`, `SouthEast`, etc.) ignore this requirement entirely. + /// + /// # Calculation + /// + /// The overlap factor is calculated as: + /// ```text + /// overlap_factor = actual_overlap / min(origin_size, candidate_size) + /// ``` + /// + /// For East/West navigation, this measures vertical overlap: + /// - `actual_overlap` = overlapping height between the two elements + /// - Sizes are the heights of the origin and candidate + /// + /// For North/South navigation, this measures horizontal overlap: + /// - `actual_overlap` = overlapping width between the two elements + /// - Sizes are the widths of the origin and candidate + /// + /// # Examples + /// + /// - `0.0` (default): Any overlap is sufficient. Even if elements barely touch, they can be neighbors. + /// - `0.5`: Elements must overlap by at least 50% of the smaller element's size. + /// - `1.0`: Perfect alignment required. The smaller element must be completely within the bounds + /// of the larger element along the perpendicular axis. + /// + /// # Use Cases + /// + /// - **Sparse/irregular layouts** (e.g., star constellations): Use `0.0` to allow navigation + /// between elements that don't directly align. + /// - **Grid layouts**: Use `0.5` or higher to ensure navigation only connects elements in + /// the same row or column. + /// - **Strict alignment**: Use `1.0` to require perfect alignment, though this may result + /// in disconnected navigation graphs if elements aren't precisely aligned. + pub min_alignment_factor: f32, + + /// Maximum search distance in logical pixels. + /// + /// Nodes beyond this distance won't be connected. `None` means unlimited. + pub max_search_distance: Option, + + /// Whether to prefer nodes that are more aligned with the exact direction. + /// + /// When `true`, nodes that are more directly in line with the requested direction + /// will be strongly preferred over nodes at an angle. + pub prefer_aligned: bool, +} + +impl Default for AutoNavigationConfig { + fn default() -> Self { + Self { + min_alignment_factor: 0.0, // Any overlap is acceptable + max_search_distance: None, // No distance limit + prefer_aligned: true, // Prefer well-aligned nodes + } + } +} + /// The up-to-eight neighbors of a focusable entity, one for each [`CompassOctant`]. #[derive(Default, Debug, Clone, PartialEq)] #[cfg_attr( @@ -293,6 +361,27 @@ pub enum DirectionalNavigationError { }, } +/// A focusable area with position and size information. +/// +/// This struct represents a UI element used during directional navigation, +/// containing its entity ID, center position, and size for spatial navigation calculations. +/// +/// The term "focusable area" avoids confusion with UI `Node` components in `bevy_ui`. +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +pub struct FocusableArea { + /// The entity identifier for this focusable area. + pub entity: Entity, + /// The center position in global coordinates. + pub position: Vec2, + /// The size (width, height) of the area. + pub size: Vec2, +} + /// Trait for extracting position and size from navigable UI components. /// /// This allows the auto-navigation system to work with different UI implementations @@ -317,7 +406,7 @@ pub trait Navigable { /// # Example /// /// ```rust -/// # use bevy_input_focus::{directional_navigation::*, navigator::{FocusableArea, AutoNavigationConfig}}; +/// # use bevy_input_focus::directional_navigation::*; /// # use bevy_ecs::entity::Entity; /// # use bevy_math::Vec2; /// let mut nav_map = DirectionalNavigationMap::default(); diff --git a/crates/bevy_input_focus/src/navigator.rs b/crates/bevy_input_focus/src/navigator.rs index ceee53fa22876..2e984b0088beb 100644 --- a/crates/bevy_input_focus/src/navigator.rs +++ b/crates/bevy_input_focus/src/navigator.rs @@ -1,103 +1,8 @@ -//! Common structs and functions that can be used to create navigation systems. - +//! Functions used by navigators to determine where to go next. +use crate::directional_navigation::{AutoNavigationConfig, FocusableArea}; use bevy_ecs::prelude::*; use bevy_math::{CompassOctant, Dir2, Rect, Vec2}; -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::Reflect; - -/// Configuration resource for automatic directional navigation and for generating manual -/// navigation edges via [`auto_generate_navigation_edges`](crate::directional_navigation::auto_generate_navigation_edges) -/// -/// This resource controls how nodes should be automatically connected in each direction. -#[derive(Resource, Debug, Clone, PartialEq)] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Resource, Debug, PartialEq, Clone) -)] -pub struct AutoNavigationConfig { - /// Minimum overlap ratio (0.0-1.0) required along the perpendicular axis for cardinal directions. - /// - /// This parameter controls how much two UI elements must overlap in the perpendicular direction - /// to be considered reachable neighbors. It only applies to cardinal directions (`North`, `South`, `East`, `West`); - /// diagonal directions (`NorthEast`, `SouthEast`, etc.) ignore this requirement entirely. - /// - /// # Calculation - /// - /// The overlap factor is calculated as: - /// ```text - /// overlap_factor = actual_overlap / min(origin_size, candidate_size) - /// ``` - /// - /// For East/West navigation, this measures vertical overlap: - /// - `actual_overlap` = overlapping height between the two elements - /// - Sizes are the heights of the origin and candidate - /// - /// For North/South navigation, this measures horizontal overlap: - /// - `actual_overlap` = overlapping width between the two elements - /// - Sizes are the widths of the origin and candidate - /// - /// # Examples - /// - /// - `0.0` (default): Any overlap is sufficient. Even if elements barely touch, they can be neighbors. - /// - `0.5`: Elements must overlap by at least 50% of the smaller element's size. - /// - `1.0`: Perfect alignment required. The smaller element must be completely within the bounds - /// of the larger element along the perpendicular axis. - /// - /// # Use Cases - /// - /// - **Sparse/irregular layouts** (e.g., star constellations): Use `0.0` to allow navigation - /// between elements that don't directly align. - /// - **Grid layouts**: Use `0.5` or higher to ensure navigation only connects elements in - /// the same row or column. - /// - **Strict alignment**: Use `1.0` to require perfect alignment, though this may result - /// in disconnected navigation graphs if elements aren't precisely aligned. - pub min_alignment_factor: f32, - - /// Maximum search distance in logical pixels. - /// - /// Nodes beyond this distance won't be connected. `None` means unlimited. - pub max_search_distance: Option, - - /// Whether to prefer nodes that are more aligned with the exact direction. - /// - /// When `true`, nodes that are more directly in line with the requested direction - /// will be strongly preferred over nodes at an angle. - pub prefer_aligned: bool, -} - -impl Default for AutoNavigationConfig { - fn default() -> Self { - Self { - min_alignment_factor: 0.0, // Any overlap is acceptable - max_search_distance: None, // No distance limit - prefer_aligned: true, // Prefer well-aligned nodes - } - } -} - -/// A focusable area with position and size information. -/// -/// This struct represents a UI element used during directional navigation, -/// containing its entity ID, center position, and size for spatial navigation calculations. -/// -/// The term "focusable area" avoids confusion with UI `Node` components in `bevy_ui`. -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Clone) -)] -pub struct FocusableArea { - /// The entity identifier for this focusable area. - pub entity: Entity, - /// The center position in global coordinates. - pub position: Vec2, - /// The size (width, height) of the area. - pub size: Vec2, -} - // We can't directly implement this for `bevy_ui` types here without circular dependencies, // so we'll use a more generic approach with separate functions for different component sets. @@ -234,7 +139,8 @@ fn score_candidate( /// Finds the best entity to navigate to from the origin towards the given direction. /// -/// For details on what "best" means here, refer to [`AutoNavigationConfig`]. +/// For details on what "best" means here, refer to [`AutoNavigationConfig`], which configures +/// how candidates are scored. pub fn find_best_candidate( origin: &FocusableArea, direction: CompassOctant, diff --git a/crates/bevy_ui/src/directional_navigation.rs b/crates/bevy_ui/src/directional_navigation.rs index ace12e3a9e4cd..b4e00bdd75776 100644 --- a/crates/bevy_ui/src/directional_navigation.rs +++ b/crates/bevy_ui/src/directional_navigation.rs @@ -9,8 +9,10 @@ use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_math::CompassOctant; use bevy_input_focus::{ - directional_navigation::{DirectionalNavigation, DirectionalNavigationError}, - navigator::*, + directional_navigation::{ + AutoNavigationConfig, DirectionalNavigation, DirectionalNavigationError, FocusableArea, + }, + navigator::find_best_candidate, }; use bevy_reflect::{prelude::*, Reflect}; diff --git a/examples/ui/auto_directional_navigation.rs b/examples/ui/auto_directional_navigation.rs index 2fafcc5240222..e1ef7c331f24b 100644 --- a/examples/ui/auto_directional_navigation.rs +++ b/examples/ui/auto_directional_navigation.rs @@ -17,7 +17,7 @@ use core::time::Duration; use bevy::{ camera::NormalizedRenderTarget, input_focus::{ - directional_navigation::DirectionalNavigationPlugin, navigator::AutoNavigationConfig, + directional_navigation::{AutoNavigationConfig, DirectionalNavigationPlugin}, InputDispatchPlugin, InputFocus, InputFocusVisible, }, math::{CompassOctant, Dir2}, From 2ca28a5dcd215702fbb7028772b752c2acdcfe27 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Fri, 2 Jan 2026 22:51:24 -0500 Subject: [PATCH 14/15] docs: fixes comment --- crates/bevy_input_focus/src/directional_navigation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index 247adc27ea238..a7b45788d5fe0 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -71,7 +71,7 @@ impl Plugin for DirectionalNavigationPlugin { } /// Configuration resource for automatic directional navigation and for generating manual -/// navigation edges via [`auto_generate_navigation_edges`](crate::directional_navigation::auto_generate_navigation_edges) +/// navigation edges via [`auto_generate_navigation_edges`] /// /// This resource controls how nodes should be automatically connected in each direction. #[derive(Resource, Debug, Clone, PartialEq)] From bfcf5a62147d124dc283bbc3a9a417e3f146b3c2 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Mon, 5 Jan 2026 19:06:01 -0500 Subject: [PATCH 15/15] refactor: renames module --- ...directional_navigation.rs => auto_directional_navigation.rs} | 2 +- crates/bevy_ui/src/lib.rs | 2 +- examples/ui/auto_directional_navigation.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename crates/bevy_ui/src/{directional_navigation.rs => auto_directional_navigation.rs} (99%) diff --git a/crates/bevy_ui/src/directional_navigation.rs b/crates/bevy_ui/src/auto_directional_navigation.rs similarity index 99% rename from crates/bevy_ui/src/directional_navigation.rs rename to crates/bevy_ui/src/auto_directional_navigation.rs index b4e00bdd75776..d714c3b3c5fda 100644 --- a/crates/bevy_ui/src/directional_navigation.rs +++ b/crates/bevy_ui/src/auto_directional_navigation.rs @@ -24,7 +24,7 @@ use bevy_reflect::{prelude::*, Reflect}; /// /// ```rust /// # use bevy_ecs::prelude::*; -/// # use bevy_ui::directional_navigation::AutoDirectionalNavigation; +/// # use bevy_ui::auto_directional_navigation::AutoDirectionalNavigation; /// fn spawn_auto_nav_button(mut commands: Commands) { /// commands.spawn(( /// // ... Button, Node, etc. ... diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 11c120df45bda..9115ce0cd7a23 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -10,7 +10,7 @@ //! Spawn UI elements with [`widget::Button`], [`ImageNode`](widget::ImageNode), [`Text`](prelude::Text) and [`Node`] //! This UI is laid out with the Flexbox and CSS Grid layout models (see ) -pub mod directional_navigation; +pub mod auto_directional_navigation; pub mod interaction_states; pub mod measurement; pub mod update; diff --git a/examples/ui/auto_directional_navigation.rs b/examples/ui/auto_directional_navigation.rs index e1ef7c331f24b..fe5fda3fa9982 100644 --- a/examples/ui/auto_directional_navigation.rs +++ b/examples/ui/auto_directional_navigation.rs @@ -27,7 +27,7 @@ use bevy::{ }, platform::collections::HashSet, prelude::*, - ui::directional_navigation::{AutoDirectionalNavigation, AutoDirectionalNavigator}, + ui::auto_directional_navigation::{AutoDirectionalNavigation, AutoDirectionalNavigator}, }; fn main() {