bevy_input_focus/
tab_navigation.rs

1//! This module provides a framework for handling linear tab-key navigation in Bevy applications.
2//!
3//! The rules of tabbing are derived from the HTML specification, and are as follows:
4//!
5//! * An index >= 0 means that the entity is tabbable via sequential navigation.
6//!   The order of tabbing is determined by the index, with lower indices being tabbed first.
7//!   If two entities have the same index, then the order is determined by the order of
8//!   the entities in the ECS hierarchy (as determined by Parent/Child).
9//! * An index < 0 means that the entity is not focusable via sequential navigation, but
10//!   can still be focused via direct selection.
11//!
12//! Tabbable entities must be descendants of a [`TabGroup`] entity, which is a component that
13//! marks a tree of entities as containing tabbable elements. The order of tab groups
14//! is determined by the [`TabGroup::order`] field, with lower orders being tabbed first. Modal tab groups
15//! are used for ui elements that should only tab within themselves, such as modal dialog boxes.
16//!
17//! To enable automatic tabbing, add the
18//! [`TabNavigationPlugin`] and [`InputDispatchPlugin`](crate::InputDispatchPlugin) to your app.
19//! This will install a keyboard event observer on the primary window which automatically handles
20//! tab navigation for you.
21//!
22//! Alternatively, if you want to have more control over tab navigation, or are using an input-action-mapping framework,
23//! you can use the [`TabNavigation`] system parameter directly instead.
24//! This object can be injected into your systems, and provides a [`navigate`](`TabNavigation::navigate`) method which can be
25//! used to navigate between focusable entities.
26
27use alloc::vec::Vec;
28use bevy_app::{App, Plugin, Startup};
29use bevy_ecs::{
30    component::Component,
31    entity::Entity,
32    hierarchy::{ChildOf, Children},
33    observer::Trigger,
34    query::{With, Without},
35    system::{Commands, Query, Res, ResMut, SystemParam},
36};
37use bevy_input::{
38    keyboard::{KeyCode, KeyboardInput},
39    ButtonInput, ButtonState,
40};
41use bevy_window::PrimaryWindow;
42use log::warn;
43use thiserror::Error;
44
45use crate::{FocusedInput, InputFocus, InputFocusVisible};
46
47#[cfg(feature = "bevy_reflect")]
48use {
49    bevy_ecs::prelude::ReflectComponent,
50    bevy_reflect::{prelude::*, Reflect},
51};
52
53/// A component which indicates that an entity wants to participate in tab navigation.
54///
55/// Note that you must also add the [`TabGroup`] component to the entity's ancestor in order
56/// for this component to have any effect.
57#[derive(Debug, Default, Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
58#[cfg_attr(
59    feature = "bevy_reflect",
60    derive(Reflect),
61    reflect(Debug, Default, Component, PartialEq, Clone)
62)]
63pub struct TabIndex(pub i32);
64
65/// A component used to mark a tree of entities as containing tabbable elements.
66#[derive(Debug, Default, Component, Copy, Clone)]
67#[cfg_attr(
68    feature = "bevy_reflect",
69    derive(Reflect),
70    reflect(Debug, Default, Component, Clone)
71)]
72pub struct TabGroup {
73    /// The order of the tab group relative to other tab groups.
74    pub order: i32,
75
76    /// Whether this is a 'modal' group. If true, then tabbing within the group (that is,
77    /// if the current focus entity is a child of this group) will cycle through the children
78    /// of this group. If false, then tabbing within the group will cycle through all non-modal
79    /// tab groups.
80    pub modal: bool,
81}
82
83impl TabGroup {
84    /// Create a new tab group with the given order.
85    pub fn new(order: i32) -> Self {
86        Self {
87            order,
88            modal: false,
89        }
90    }
91
92    /// Create a modal tab group.
93    pub fn modal() -> Self {
94        Self {
95            order: 0,
96            modal: true,
97        }
98    }
99}
100
101/// A navigation action that users might take to navigate your user interface in a cyclic fashion.
102///
103/// These values are consumed by the [`TabNavigation`] system param.
104pub enum NavAction {
105    /// Navigate to the next focusable entity, wrapping around to the beginning if at the end.
106    ///
107    /// This is commonly triggered by pressing the Tab key.
108    Next,
109    /// Navigate to the previous focusable entity, wrapping around to the end if at the beginning.
110    ///
111    /// This is commonly triggered by pressing Shift+Tab.
112    Previous,
113    /// Navigate to the first focusable entity.
114    ///
115    /// This is commonly triggered by pressing Home.
116    First,
117    /// Navigate to the last focusable entity.
118    ///
119    /// This is commonly triggered by pressing End.
120    Last,
121}
122
123/// An error that can occur during [tab navigation](crate::tab_navigation).
124#[derive(Debug, Error, PartialEq, Eq, Clone)]
125pub enum TabNavigationError {
126    /// No tab groups were found.
127    #[error("No tab groups found")]
128    NoTabGroups,
129    /// No focusable entities were found.
130    #[error("No focusable entities found")]
131    NoFocusableEntities,
132    /// Could not navigate to the next focusable entity.
133    ///
134    /// This can occur if your tab groups are malformed.
135    #[error("Failed to navigate to next focusable entity")]
136    FailedToNavigateToNextFocusableEntity,
137    /// No tab group for the current focus entity was found.
138    #[error("No tab group found for currently focused entity {previous_focus}. Users will not be able to navigate back to this entity.")]
139    NoTabGroupForCurrentFocus {
140        /// The entity that was previously focused,
141        /// and is missing its tab group.
142        previous_focus: Entity,
143        /// The new entity that will be focused.
144        ///
145        /// If you want to recover from this error, set [`InputFocus`] to this entity.
146        new_focus: Entity,
147    },
148}
149
150/// An injectable helper object that provides tab navigation functionality.
151#[doc(hidden)]
152#[derive(SystemParam)]
153pub struct TabNavigation<'w, 's> {
154    // Query for tab groups.
155    tabgroup_query: Query<'w, 's, (Entity, &'static TabGroup, &'static Children)>,
156    // Query for tab indices.
157    tabindex_query: Query<
158        'w,
159        's,
160        (Entity, Option<&'static TabIndex>, Option<&'static Children>),
161        Without<TabGroup>,
162    >,
163    // Query for parents.
164    parent_query: Query<'w, 's, &'static ChildOf>,
165}
166
167impl TabNavigation<'_, '_> {
168    /// Navigate to the desired focusable entity.
169    ///
170    /// Change the [`NavAction`] to navigate in a different direction.
171    /// Focusable entities are determined by the presence of the [`TabIndex`] component.
172    ///
173    /// If no focusable entities are found, then this function will return either the first
174    /// or last focusable entity, depending on the direction of navigation. For example, if
175    /// `action` is `Next` and no focusable entities are found, then this function will return
176    /// the first focusable entity.
177    pub fn navigate(
178        &self,
179        focus: &InputFocus,
180        action: NavAction,
181    ) -> Result<Entity, TabNavigationError> {
182        // If there are no tab groups, then there are no focusable entities.
183        if self.tabgroup_query.is_empty() {
184            return Err(TabNavigationError::NoTabGroups);
185        }
186
187        // Start by identifying which tab group we are in. Mainly what we want to know is if
188        // we're in a modal group.
189        let tabgroup = focus.0.and_then(|focus_ent| {
190            self.parent_query
191                .iter_ancestors(focus_ent)
192                .find_map(|entity| {
193                    self.tabgroup_query
194                        .get(entity)
195                        .ok()
196                        .map(|(_, tg, _)| (entity, tg))
197                })
198        });
199
200        let navigation_result = self.navigate_in_group(tabgroup, focus, action);
201
202        match navigation_result {
203            Ok(entity) => {
204                if focus.0.is_some() && tabgroup.is_none() {
205                    Err(TabNavigationError::NoTabGroupForCurrentFocus {
206                        previous_focus: focus.0.unwrap(),
207                        new_focus: entity,
208                    })
209                } else {
210                    Ok(entity)
211                }
212            }
213            Err(e) => Err(e),
214        }
215    }
216
217    fn navigate_in_group(
218        &self,
219        tabgroup: Option<(Entity, &TabGroup)>,
220        focus: &InputFocus,
221        action: NavAction,
222    ) -> Result<Entity, TabNavigationError> {
223        // List of all focusable entities found.
224        let mut focusable: Vec<(Entity, TabIndex)> =
225            Vec::with_capacity(self.tabindex_query.iter().len());
226
227        match tabgroup {
228            Some((tg_entity, tg)) if tg.modal => {
229                // We're in a modal tab group, then gather all tab indices in that group.
230                if let Ok((_, _, children)) = self.tabgroup_query.get(tg_entity) {
231                    for child in children.iter() {
232                        self.gather_focusable(&mut focusable, *child);
233                    }
234                }
235            }
236            _ => {
237                // Otherwise, gather all tab indices in all non-modal tab groups.
238                let mut tab_groups: Vec<(Entity, TabGroup)> = self
239                    .tabgroup_query
240                    .iter()
241                    .filter(|(_, tg, _)| !tg.modal)
242                    .map(|(e, tg, _)| (e, *tg))
243                    .collect();
244                // Stable sort by group order
245                tab_groups.sort_by_key(|(_, tg)| tg.order);
246
247                // Search group descendants
248                tab_groups.iter().for_each(|(tg_entity, _)| {
249                    self.gather_focusable(&mut focusable, *tg_entity);
250                });
251            }
252        }
253
254        if focusable.is_empty() {
255            return Err(TabNavigationError::NoFocusableEntities);
256        }
257
258        // Stable sort by tabindex
259        focusable.sort_by_key(|(_, idx)| *idx);
260
261        let index = focusable.iter().position(|e| Some(e.0) == focus.0);
262        let count = focusable.len();
263        let next = match (index, action) {
264            (Some(idx), NavAction::Next) => (idx + 1).rem_euclid(count),
265            (Some(idx), NavAction::Previous) => (idx + count - 1).rem_euclid(count),
266            (None, NavAction::Next) | (_, NavAction::First) => 0,
267            (None, NavAction::Previous) | (_, NavAction::Last) => count - 1,
268        };
269        match focusable.get(next) {
270            Some((entity, _)) => Ok(*entity),
271            None => Err(TabNavigationError::FailedToNavigateToNextFocusableEntity),
272        }
273    }
274
275    /// Gather all focusable entities in tree order.
276    fn gather_focusable(&self, out: &mut Vec<(Entity, TabIndex)>, parent: Entity) {
277        if let Ok((entity, tabindex, children)) = self.tabindex_query.get(parent) {
278            if let Some(tabindex) = tabindex {
279                if tabindex.0 >= 0 {
280                    out.push((entity, *tabindex));
281                }
282            }
283            if let Some(children) = children {
284                for child in children.iter() {
285                    // Don't traverse into tab groups, as they are handled separately.
286                    if self.tabgroup_query.get(*child).is_err() {
287                        self.gather_focusable(out, *child);
288                    }
289                }
290            }
291        } else if let Ok((_, tabgroup, children)) = self.tabgroup_query.get(parent) {
292            if !tabgroup.modal {
293                for child in children.iter() {
294                    self.gather_focusable(out, *child);
295                }
296            }
297        }
298    }
299}
300
301/// Plugin for navigating between focusable entities using keyboard input.
302pub struct TabNavigationPlugin;
303
304impl Plugin for TabNavigationPlugin {
305    fn build(&self, app: &mut App) {
306        app.add_systems(Startup, setup_tab_navigation);
307
308        #[cfg(feature = "bevy_reflect")]
309        app.register_type::<TabIndex>().register_type::<TabGroup>();
310    }
311}
312
313fn setup_tab_navigation(mut commands: Commands, window: Query<Entity, With<PrimaryWindow>>) {
314    for window in window.iter() {
315        commands.entity(window).observe(handle_tab_navigation);
316    }
317}
318
319/// Observer function which handles tab navigation.
320///
321/// This observer responds to [`KeyCode::Tab`] events and Shift+Tab events,
322/// cycling through focusable entities in the order determined by their tab index.
323///
324/// Any [`TabNavigationError`]s that occur during tab navigation are logged as warnings.
325pub fn handle_tab_navigation(
326    mut trigger: Trigger<FocusedInput<KeyboardInput>>,
327    nav: TabNavigation,
328    mut focus: ResMut<InputFocus>,
329    mut visible: ResMut<InputFocusVisible>,
330    keys: Res<ButtonInput<KeyCode>>,
331) {
332    // Tab navigation.
333    let key_event = &trigger.event().input;
334    if key_event.key_code == KeyCode::Tab
335        && key_event.state == ButtonState::Pressed
336        && !key_event.repeat
337    {
338        let maybe_next = nav.navigate(
339            &focus,
340            if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
341                NavAction::Previous
342            } else {
343                NavAction::Next
344            },
345        );
346
347        match maybe_next {
348            Ok(next) => {
349                trigger.propagate(false);
350                focus.set(next);
351                visible.0 = true;
352            }
353            Err(e) => {
354                warn!("Tab navigation error: {}", e);
355                // This failure mode is recoverable, but still indicates a problem.
356                if let TabNavigationError::NoTabGroupForCurrentFocus { new_focus, .. } = e {
357                    trigger.propagate(false);
358                    focus.set(new_focus);
359                    visible.0 = true;
360                }
361            }
362        }
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use bevy_ecs::system::SystemState;
369
370    use super::*;
371
372    #[test]
373    fn test_tab_navigation() {
374        let mut app = App::new();
375        let world = app.world_mut();
376
377        let tab_group_entity = world.spawn(TabGroup::new(0)).id();
378        let tab_entity_1 = world.spawn((TabIndex(0), ChildOf(tab_group_entity))).id();
379        let tab_entity_2 = world.spawn((TabIndex(1), ChildOf(tab_group_entity))).id();
380
381        let mut system_state: SystemState<TabNavigation> = SystemState::new(world);
382        let tab_navigation = system_state.get(world);
383        assert_eq!(tab_navigation.tabgroup_query.iter().count(), 1);
384        assert_eq!(tab_navigation.tabindex_query.iter().count(), 2);
385
386        let next_entity =
387            tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next);
388        assert_eq!(next_entity, Ok(tab_entity_2));
389
390        let prev_entity =
391            tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous);
392        assert_eq!(prev_entity, Ok(tab_entity_1));
393
394        let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First);
395        assert_eq!(first_entity, Ok(tab_entity_1));
396
397        let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last);
398        assert_eq!(last_entity, Ok(tab_entity_2));
399    }
400}