diff --git a/crates/bevy_input_focus/Cargo.toml b/crates/bevy_input_focus/Cargo.toml index 75c85845b1..ed86ecdec0 100644 --- a/crates/bevy_input_focus/Cargo.toml +++ b/crates/bevy_input_focus/Cargo.toml @@ -17,6 +17,8 @@ bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev", default-f bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev", default-features = false } bevy_window = { path = "../bevy_window", version = "0.15.0-dev", default-features = false } +thiserror = { version = "2", default-features = false } + [dev-dependencies] smol_str = "0.2" diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index 184e406a5b..d47744a357 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -38,6 +38,7 @@ use bevy_input::{ }; use bevy_utils::tracing::warn; use bevy_window::PrimaryWindow; +use thiserror::Error; use crate::{FocusedInput, InputFocus, InputFocusVisible}; @@ -79,20 +80,55 @@ impl TabGroup { } } -/// A navigation action for tabbing. +/// A navigation action that users might take to navigate your user interface in a cyclic fashion. /// /// These values are consumed by the [`TabNavigation`] system param. pub enum NavAction { /// Navigate to the next focusable entity, wrapping around to the beginning if at the end. + /// + /// This is commonly triggered by pressing the Tab key. Next, /// Navigate to the previous focusable entity, wrapping around to the end if at the beginning. + /// + /// This is commonly triggered by pressing Shift+Tab. Previous, /// Navigate to the first focusable entity. + /// + /// This is commonly triggered by pressing Home. First, /// Navigate to the last focusable entity. + /// + /// This is commonly triggered by pressing End. Last, } +/// An error that can occur during [tab navigation](crate::tab_navigation). +#[derive(Debug, Error, PartialEq, Eq, Clone)] +pub enum TabNavigationError { + /// No tab groups were found. + #[error("No tab groups found")] + NoTabGroups, + /// No focusable entities were found. + #[error("No focusable entities found")] + NoFocusableEntities, + /// Could not navigate to the next focusable entity. + /// + /// This can occur if your tab groups are malformed. + #[error("Failed to navigate to next focusable entity")] + FailedToNavigateToNextFocusableEntity, + /// No tab group for the current focus entity was found. + #[error("No tab group found for currently focused entity {previous_focus}. Users will not be able to navigate back to this entity.")] + NoTabGroupForCurrentFocus { + /// The entity that was previously focused, + /// and is missing its tab group. + previous_focus: Entity, + /// The new entity that will be focused. + /// + /// If you want to recover from this error, set [`InputFocus`] to this entity. + new_focus: Entity, + }, +} + /// An injectable helper object that provides tab navigation functionality. #[doc(hidden)] #[derive(SystemParam)] @@ -112,23 +148,23 @@ pub struct TabNavigation<'w, 's> { } impl TabNavigation<'_, '_> { - /// Navigate to the next focusable entity. + /// Navigate to the desired focusable entity. /// + /// Change the [`NavAction`] to navigate in a different direction. /// Focusable entities are determined by the presence of the [`TabIndex`] component. /// - /// Arguments: - /// * `focus`: The current focus entity, or `None` if no entity has focus. - /// * `action`: Whether to select the next, previous, first, or last focusable entity. - /// /// If no focusable entities are found, then this function will return either the first /// or last focusable entity, depending on the direction of navigation. For example, if /// `action` is `Next` and no focusable entities are found, then this function will return /// the first focusable entity. - pub fn navigate(&self, focus: &InputFocus, action: NavAction) -> Option { + pub fn navigate( + &self, + focus: &InputFocus, + action: NavAction, + ) -> Result { // If there are no tab groups, then there are no focusable entities. if self.tabgroup_query.is_empty() { - warn!("No tab groups found"); - return None; + return Err(TabNavigationError::NoTabGroups); } // Start by identifying which tab group we are in. Mainly what we want to know is if @@ -144,11 +180,21 @@ impl TabNavigation<'_, '_> { }) }); - if focus.0.is_some() && tabgroup.is_none() { - warn!("No tab group found for focus entity. Users will not be able to navigate back to this entity."); - } + let navigation_result = self.navigate_in_group(tabgroup, focus, action); - self.navigate_in_group(tabgroup, focus, action) + match navigation_result { + Ok(entity) => { + if focus.0.is_some() && tabgroup.is_none() { + Err(TabNavigationError::NoTabGroupForCurrentFocus { + previous_focus: focus.0.unwrap(), + new_focus: entity, + }) + } else { + Ok(entity) + } + } + Err(e) => Err(e), + } } fn navigate_in_group( @@ -156,7 +202,7 @@ impl TabNavigation<'_, '_> { tabgroup: Option<(Entity, &TabGroup)>, focus: &InputFocus, action: NavAction, - ) -> Option { + ) -> Result { // List of all focusable entities found. let mut focusable: Vec<(Entity, TabIndex)> = Vec::with_capacity(self.tabindex_query.iter().len()); @@ -189,8 +235,7 @@ impl TabNavigation<'_, '_> { } if focusable.is_empty() { - warn!("No focusable entities found"); - return None; + return Err(TabNavigationError::NoFocusableEntities); } // Stable sort by tabindex @@ -204,7 +249,10 @@ impl TabNavigation<'_, '_> { (None, NavAction::Next) | (_, NavAction::First) => 0, (None, NavAction::Previous) | (_, NavAction::Last) => count - 1, }; - focusable.get(next).map(|(e, _)| e).copied() + match focusable.get(next) { + Some((entity, _)) => Ok(*entity), + None => Err(TabNavigationError::FailedToNavigateToNextFocusableEntity), + } } /// Gather all focusable entities in tree order. @@ -252,6 +300,8 @@ fn setup_tab_navigation(mut commands: Commands, window: Query>, nav: TabNavigation, @@ -265,7 +315,7 @@ pub fn handle_tab_navigation( && key_event.state == ButtonState::Pressed && !key_event.repeat { - let next = nav.navigate( + let maybe_next = nav.navigate( &focus, if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) { NavAction::Previous @@ -273,10 +323,22 @@ pub fn handle_tab_navigation( NavAction::Next }, ); - if next.is_some() { - trigger.propagate(false); - focus.0 = next; - visible.0 = true; + + match maybe_next { + Ok(next) => { + trigger.propagate(false); + focus.set(next); + visible.0 = true; + } + Err(e) => { + warn!("Tab navigation error: {}", e); + // This failure mode is recoverable, but still indicates a problem. + if let TabNavigationError::NoTabGroupForCurrentFocus { new_focus, .. } = e { + trigger.propagate(false); + focus.set(new_focus); + visible.0 = true; + } + } } } } @@ -305,16 +367,16 @@ mod tests { let next_entity = tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next); - assert_eq!(next_entity, Some(tab_entity_2)); + assert_eq!(next_entity, Ok(tab_entity_2)); let prev_entity = tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous); - assert_eq!(prev_entity, Some(tab_entity_1)); + assert_eq!(prev_entity, Ok(tab_entity_1)); let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First); - assert_eq!(first_entity, Some(tab_entity_1)); + assert_eq!(first_entity, Ok(tab_entity_1)); let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last); - assert_eq!(last_entity, Some(tab_entity_2)); + assert_eq!(last_entity, Ok(tab_entity_2)); } }