diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index 9a9da75aff853..f202e6f40eed2 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -152,6 +152,34 @@ impl Default for AutoNavigationConfig { } } +/// Represents what's near a focusable entity. +#[derive(Default, Debug, Clone, PartialEq, Copy)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Default, Debug, PartialEq, Clone) +)] +pub enum NavNeighbor { + /// No neighbor explicitly set. + #[default] + Auto, + /// Do not find a neighbor. + Blocked, + /// The neighbor is known and set. + Set(Entity), +} + +impl NavNeighbor { + /// Helper for getting the pointed-to entity, if any. + pub fn get(&self) -> Option { + if let NavNeighbor::Set(n) = self { + Some(*n) + } else { + None + } + } +} + /// The up-to-eight neighbors of a focusable entity, one for each [`CompassOctant`]. #[derive(Default, Debug, Clone, PartialEq)] #[cfg_attr( @@ -163,26 +191,38 @@ pub struct NavNeighbors { /// The array of neighbors, one for each [`CompassOctant`]. /// The mapping between array elements and directions is determined by [`CompassOctant::to_index`]. /// - /// If no neighbor exists in a given direction, the value will be [`None`]. - /// In most cases, using [`NavNeighbors::set`] and [`NavNeighbors::get`] - /// will be more ergonomic than directly accessing this array. - pub neighbors: [Option; 8], + /// If no neighbor is set in a given direction, the value will be + /// [`NavNeighbor::Auto`]. If navigation should be explicitly blocked in a + /// given direction, the value will be [`NavNeighbor::Blocked`]. In most + /// cases, using [`NavNeighbors::set`], [`NavNeighbors::get`], and + /// [`NavNeighbors::block`] will be more ergonomic than directly accessing + /// this array. + pub neighbors: [NavNeighbor; 8], } impl NavNeighbors { /// An empty set of neighbors. pub const EMPTY: NavNeighbors = NavNeighbors { - neighbors: [None; 8], + neighbors: [NavNeighbor::Auto; 8], }; /// Get the neighbor for a given [`CompassOctant`]. - pub const fn get(&self, octant: CompassOctant) -> Option { + pub const fn get(&self, octant: CompassOctant) -> NavNeighbor { self.neighbors[octant.to_index()] } /// Set the neighbor for a given [`CompassOctant`]. pub const fn set(&mut self, octant: CompassOctant, entity: Entity) { - self.neighbors[octant.to_index()] = Some(entity); + self.neighbors[octant.to_index()] = NavNeighbor::Set(entity); + } + + /// Prevent navigation to a given [`CompassOctant`]. + /// + /// Note that navigation in this direction specifically will + /// be blocked. For example, blocking [`CompassOctant::North`] + /// will not affect the neighbor towards [`CompassOctant::NorthWest`]. + pub const fn block(&mut self, octant: CompassOctant) { + self.neighbors[octant.to_index()] = NavNeighbor::Blocked; } } @@ -231,8 +271,8 @@ impl DirectionalNavigationMap { for node in self.neighbors.values_mut() { for neighbor in node.neighbors.iter_mut() { - if *neighbor == Some(entity) { - *neighbor = None; + if *neighbor == NavNeighbor::Set(entity) { + *neighbor = NavNeighbor::Auto; } } } @@ -252,10 +292,11 @@ impl DirectionalNavigationMap { for node in self.neighbors.values_mut() { for neighbor in node.neighbors.iter_mut() { - if let Some(entity) = *neighbor { - if entities.contains(&entity) { - *neighbor = None; - } + let NavNeighbor::Set(entity) = neighbor else { + continue; + }; + if entities.contains(entity) { + *neighbor = NavNeighbor::Auto; } } } @@ -278,6 +319,22 @@ impl DirectionalNavigationMap { .set(direction, b); } + /// Adds an edge blocking automatic navigation from an entity in a direction. + /// Any existing edge from A in the provided direction will be overwritten. + /// + /// The reverse block will not be added, so navigation will only be possible from other entities + /// in the direction. + /// If you want to add a symmetrical block, use [`block_symmetrical_edge`](Self::block_symmetrical_edge) instead. + /// + /// Note that blocking a primary cardinal direction will not block intermediates. + /// In other words, blocking `North` will still allow navigation towards `NorthEast`. + pub fn block_edge(&mut self, a: Entity, direction: CompassOctant) { + self.neighbors + .entry(a) + .or_insert(NavNeighbors::EMPTY) + .block(direction); + } + /// Adds a symmetrical edge between two entities in the navigation map. /// The A -> B path will use the provided direction, while B -> A will use the [`CompassOctant::opposite`] variant. /// @@ -287,6 +344,16 @@ impl DirectionalNavigationMap { self.add_edge(b, a, direction.opposite()); } + // TODO: not quite sure if this is necessary + /// Adds a symmetrical edge between two entities in the navigation map. + /// The A -> B path will use the provided direction, while B -> A will use the [`CompassOctant::opposite`] variant. + /// + /// Any existing connections between the two entities will be overwritten. + pub fn block_symmetrical_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) { + self.block_edge(a, direction); + self.block_edge(b, direction.opposite()); + } + /// Add symmetrical edges between each consecutive pair of entities in the provided slice. /// /// Unlike [`add_looping_edges`](Self::add_looping_edges), this method does not loop back to the first entity. @@ -309,16 +376,17 @@ impl DirectionalNavigationMap { } /// Gets the entity in a given direction from the current focus, if any. - pub fn get_neighbor(&self, focus: Entity, octant: CompassOctant) -> Option { + pub fn get_neighbor(&self, focus: Entity, octant: CompassOctant) -> NavNeighbor { self.neighbors .get(&focus) - .and_then(|neighbors| neighbors.get(octant)) + .map(|neighbors| neighbors.get(octant)) + .unwrap_or(NavNeighbor::Auto) } /// Looks up the neighbors of a given entity. /// /// If the entity is not in the map, [`None`] will be returned. - /// Note that the set of neighbors is not guaranteed to be non-empty though! + /// Note that the set of neighbors may be the default value! pub fn get_neighbors(&self, entity: Entity) -> Option<&NavNeighbors> { self.neighbors.get(&entity) } @@ -346,14 +414,19 @@ impl<'w> DirectionalNavigation<'w> { ) -> Result { if let Some(current_focus) = self.focus.0 { // 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 { - Err(DirectionalNavigationError::NoNeighborInDirection { + match self.map.get_neighbor(current_focus, direction) { + NavNeighbor::Auto => Err(DirectionalNavigationError::NoNeighborInDirection { current_focus, direction, - }) + }), + NavNeighbor::Blocked => Err(DirectionalNavigationError::BlockedNavigation { + current_focus, + direction, + }), + NavNeighbor::Set(new_focus) => { + self.focus.set(new_focus); + Ok(new_focus) + } } } else { Err(DirectionalNavigationError::NoFocus) @@ -375,6 +448,14 @@ pub enum DirectionalNavigationError { /// The direction in which the navigation was attempted. direction: CompassOctant, }, + /// Navigation explicitly blocked in the requested direction. + #[error("Naviation explicitly blocked from {current_focus} in the {direction:?} direction.")] + BlockedNavigation { + /// The entity that was the focus when the error occurred. + current_focus: Entity, + /// The direction in which the navigation was attempted. + direction: CompassOctant, + }, } /// A focusable area with position and size information. @@ -455,7 +536,12 @@ pub fn auto_generate_navigation_edges( // Skip if manual edge already exists (check inline to avoid borrow issues) if nav_map .get_neighbors(origin.entity) - .and_then(|neighbors| neighbors.get(octant)) + .filter(|neighbors| { + matches!( + neighbors.get(octant), + NavNeighbor::Blocked | NavNeighbor::Set(_) + ) + }) .is_some() { continue; // Respect manual override @@ -482,7 +568,7 @@ mod tests { #[test] fn setting_and_getting_nav_neighbors() { let mut neighbors = NavNeighbors::EMPTY; - assert_eq!(neighbors.get(CompassOctant::SouthEast), None); + assert_eq!(neighbors.get(CompassOctant::SouthEast), NavNeighbor::Auto); neighbors.set(CompassOctant::SouthEast, Entity::PLACEHOLDER); @@ -490,10 +576,13 @@ mod tests { if i == CompassOctant::SouthEast.to_index() { assert_eq!( neighbors.get(CompassOctant::SouthEast), - Some(Entity::PLACEHOLDER) + NavNeighbor::Set(Entity::PLACEHOLDER) ); } else { - assert_eq!(neighbors.get(CompassOctant::from_index(i).unwrap()), None); + assert_eq!( + neighbors.get(CompassOctant::from_index(i).unwrap()), + NavNeighbor::Auto + ); } } } @@ -507,10 +596,13 @@ mod tests { let mut map = DirectionalNavigationMap::default(); map.add_edge(a, b, CompassOctant::SouthEast); - assert_eq!(map.get_neighbor(a, CompassOctant::SouthEast), Some(b)); + assert_eq!( + map.get_neighbor(a, CompassOctant::SouthEast), + NavNeighbor::Set(b) + ); assert_eq!( map.get_neighbor(b, CompassOctant::SouthEast.opposite()), - None + NavNeighbor::Auto ); } @@ -523,8 +615,14 @@ mod tests { let mut map = DirectionalNavigationMap::default(); map.add_symmetrical_edge(a, b, CompassOctant::North); - assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b)); - assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a)); + assert_eq!( + map.get_neighbor(a, CompassOctant::North), + NavNeighbor::Set(b) + ); + assert_eq!( + map.get_neighbor(b, CompassOctant::South), + NavNeighbor::Set(a) + ); } #[test] @@ -537,13 +635,19 @@ mod tests { map.add_edge(a, b, CompassOctant::North); map.add_edge(b, a, CompassOctant::South); - assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b)); - assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a)); + assert_eq!( + map.get_neighbor(a, CompassOctant::North), + NavNeighbor::Set(b) + ); + assert_eq!( + map.get_neighbor(b, CompassOctant::South), + NavNeighbor::Set(a) + ); map.remove(b); - assert_eq!(map.get_neighbor(a, CompassOctant::North), None); - assert_eq!(map.get_neighbor(b, CompassOctant::South), None); + assert_eq!(map.get_neighbor(a, CompassOctant::North), NavNeighbor::Auto); + assert_eq!(map.get_neighbor(b, CompassOctant::South), NavNeighbor::Auto); } #[test] @@ -565,10 +669,10 @@ mod tests { map.remove_multiple(to_remove); - assert_eq!(map.get_neighbor(a, CompassOctant::North), None); - assert_eq!(map.get_neighbor(b, CompassOctant::South), None); - assert_eq!(map.get_neighbor(b, CompassOctant::East), None); - assert_eq!(map.get_neighbor(c, CompassOctant::West), None); + assert_eq!(map.get_neighbor(a, CompassOctant::North), NavNeighbor::Auto); + assert_eq!(map.get_neighbor(b, CompassOctant::South), NavNeighbor::Auto); + assert_eq!(map.get_neighbor(b, CompassOctant::East), NavNeighbor::Auto); + assert_eq!(map.get_neighbor(c, CompassOctant::West), NavNeighbor::Auto); } #[test] @@ -581,13 +685,25 @@ mod tests { let mut map = DirectionalNavigationMap::default(); map.add_edges(&[a, b, c], CompassOctant::East); - assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b)); - assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c)); - assert_eq!(map.get_neighbor(c, CompassOctant::East), None); + assert_eq!( + map.get_neighbor(a, CompassOctant::East), + NavNeighbor::Set(b) + ); + assert_eq!( + map.get_neighbor(b, CompassOctant::East), + NavNeighbor::Set(c) + ); + assert_eq!(map.get_neighbor(c, CompassOctant::East), NavNeighbor::Auto); - assert_eq!(map.get_neighbor(a, CompassOctant::West), None); - assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a)); - assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b)); + assert_eq!(map.get_neighbor(a, CompassOctant::West), NavNeighbor::Auto); + assert_eq!( + map.get_neighbor(b, CompassOctant::West), + NavNeighbor::Set(a) + ); + assert_eq!( + map.get_neighbor(c, CompassOctant::West), + NavNeighbor::Set(b) + ); } #[test] @@ -600,13 +716,31 @@ mod tests { let mut map = DirectionalNavigationMap::default(); map.add_looping_edges(&[a, b, c], CompassOctant::East); - assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b)); - assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c)); - assert_eq!(map.get_neighbor(c, CompassOctant::East), Some(a)); + assert_eq!( + map.get_neighbor(a, CompassOctant::East), + NavNeighbor::Set(b) + ); + assert_eq!( + map.get_neighbor(b, CompassOctant::East), + NavNeighbor::Set(c) + ); + assert_eq!( + map.get_neighbor(c, CompassOctant::East), + NavNeighbor::Set(a) + ); - assert_eq!(map.get_neighbor(a, CompassOctant::West), Some(c)); - assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a)); - assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b)); + assert_eq!( + map.get_neighbor(a, CompassOctant::West), + NavNeighbor::Set(c) + ); + assert_eq!( + map.get_neighbor(b, CompassOctant::West), + NavNeighbor::Set(a) + ); + assert_eq!( + map.get_neighbor(c, CompassOctant::West), + NavNeighbor::Set(b) + ); } #[test] @@ -683,27 +817,27 @@ mod tests { // Test horizontal navigation assert_eq!( nav_map.get_neighbor(node_a, CompassOctant::East), - Some(node_b) + NavNeighbor::Set(node_b) ); assert_eq!( nav_map.get_neighbor(node_b, CompassOctant::West), - Some(node_a) + NavNeighbor::Set(node_a) ); // Test vertical navigation assert_eq!( nav_map.get_neighbor(node_a, CompassOctant::South), - Some(node_c) + NavNeighbor::Set(node_c) ); assert_eq!( nav_map.get_neighbor(node_c, CompassOctant::North), - Some(node_a) + NavNeighbor::Set(node_a) ); // Test diagonal navigation assert_eq!( nav_map.get_neighbor(node_a, CompassOctant::SouthEast), - Some(node_d) + NavNeighbor::Set(node_d) ); } @@ -742,7 +876,7 @@ mod tests { // The manual edge should be preserved, even though B is closer assert_eq!( nav_map.get_neighbor(node_a, CompassOctant::East), - Some(node_c) + NavNeighbor::Set(node_c) ); } @@ -779,8 +913,49 @@ mod tests { assert_eq!( nav_map.get_neighbor(left, CompassOctant::East), - Some(wide_top), + NavNeighbor::Set(wide_top), "Should navigate to wide_top not bottom, even though bottom's center is closer." ); } + + #[test] + fn test_respects_set_blocks() { + let mut nav_map = DirectionalNavigationMap::default(); + let config = AutoNavigationConfig::default(); + + let node_a = Entity::from_bits(1); + let node_b = Entity::from_bits(2); + let node_c = Entity::from_bits(3); + + // Manually set a block from A to B + // A should NOT be able to nav East to B + // but SHOULD be able to nav South to C + nav_map.block_edge(node_a, CompassOctant::East); + + let nodes = vec![ + FocusableArea { + entity: node_a, + position: Vec2::new(0.0, 0.0), + size: Vec2::new(50.0, 50.0), + }, + FocusableArea { + entity: node_b, + position: Vec2::new(50.0, 0.0), + size: Vec2::new(50.0, 50.0), + }, + FocusableArea { + entity: node_c, + position: Vec2::new(0.0, -50.0), + size: Vec2::new(50.0, 50.0), + }, + ]; + + auto_generate_navigation_edges(&mut nav_map, &nodes, &config); + + // The manual edge should be preserved, even though B is closer + assert_eq!( + nav_map.get_neighbor(node_a, CompassOctant::East), + NavNeighbor::Blocked + ); + } } diff --git a/crates/bevy_ui/src/auto_directional_navigation.rs b/crates/bevy_ui/src/auto_directional_navigation.rs index eb650d0d25896..ed7e71097b360 100644 --- a/crates/bevy_ui/src/auto_directional_navigation.rs +++ b/crates/bevy_ui/src/auto_directional_navigation.rs @@ -163,25 +163,31 @@ impl<'w, 's> AutoDirectionalNavigator<'w, 's> { ) -> Result { if let Some(current_focus) = self.input_focus() { // 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, - }) + match self.manual_directional_navigation.navigate(direction) { + Ok(new_focus) => { + self.manual_directional_navigation.focus.set(new_focus); + Ok(new_focus) + } + Err(DirectionalNavigationError::NoNeighborInDirection { .. }) => { + 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, + }) + } + } + err => err, } } else { Err(DirectionalNavigationError::NoFocus) diff --git a/examples/ui/navigation/directional_navigation_overrides.rs b/examples/ui/navigation/directional_navigation_overrides.rs index 147d2f052d559..338be9b94524c 100644 --- a/examples/ui/navigation/directional_navigation_overrides.rs +++ b/examples/ui/navigation/directional_navigation_overrides.rs @@ -332,6 +332,11 @@ fn setup_paged_ui( pages_entities[1][3], CompassOctant::SouthEast, ); + // Add one-way blocking within the triangle page (Page 1) for down nav. + for btn in &pages_entities[0] { + manual_directional_nav_map.block_edge(*btn, CompassOctant::South); + manual_directional_nav_map.block_edge(*btn, CompassOctant::North); + } // For Page 3, we override the navigation North and South to be inverted. let mut col_entities = Vec::with_capacity(4); @@ -482,6 +487,28 @@ fn setup_buttons_for_grid_page( .id(); text_entities.push(footer_info); } + if page_num == 0 { + let footer_info = commands + .spawn(( + Text::new( + "Vertical movements disabled on each button, but you can still go to the next row by going off the right side." + ), + Node { + position_type: PositionType::Absolute, + left: px(450), + top: px(600), + width: px(540), + padding: UiRect::all(px(12)), + ..default() + }, + TextFont { + font_size: 20.0, + ..default() + }, + )) + .id(); + text_entities.push(footer_info); + } } /// Creates the buttons and text for the triangle page (page 2) and places the ids into their