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::On,
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, Window};
42use log::warn;
43use thiserror::Error;
44
45use crate::{AcquireFocus, 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.
104#[derive(Clone, Copy)]
105pub enum NavAction {
106    /// Navigate to the next focusable entity, wrapping around to the beginning if at the end.
107    ///
108    /// This is commonly triggered by pressing the Tab key.
109    Next,
110    /// Navigate to the previous focusable entity, wrapping around to the end if at the beginning.
111    ///
112    /// This is commonly triggered by pressing Shift+Tab.
113    Previous,
114    /// Navigate to the first focusable entity.
115    ///
116    /// This is commonly triggered by pressing Home.
117    First,
118    /// Navigate to the last focusable entity.
119    ///
120    /// This is commonly triggered by pressing End.
121    Last,
122}
123
124/// An error that can occur during [tab navigation](crate::tab_navigation).
125#[derive(Debug, Error, PartialEq, Eq, Clone)]
126pub enum TabNavigationError {
127    /// No tab groups were found.
128    #[error("No tab groups found")]
129    NoTabGroups,
130    /// No focusable entities were found.
131    #[error("No focusable entities found")]
132    NoFocusableEntities,
133    /// Could not navigate to the next focusable entity.
134    ///
135    /// This can occur if your tab groups are malformed.
136    #[error("Failed to navigate to next focusable entity")]
137    FailedToNavigateToNextFocusableEntity,
138    /// No tab group for the current focus entity was found.
139    #[error("No tab group found for currently focused entity {previous_focus}. Users will not be able to navigate back to this entity.")]
140    NoTabGroupForCurrentFocus {
141        /// The entity that was previously focused,
142        /// and is missing its tab group.
143        previous_focus: Entity,
144        /// The new entity that will be focused.
145        ///
146        /// If you want to recover from this error, set [`InputFocus`] to this entity.
147        new_focus: Entity,
148    },
149}
150
151/// An injectable helper object that provides tab navigation functionality.
152#[doc(hidden)]
153#[derive(SystemParam)]
154pub struct TabNavigation<'w, 's> {
155    // Query for tab groups.
156    tabgroup_query: Query<'w, 's, (Entity, &'static TabGroup, &'static Children)>,
157    // Query for tab indices.
158    tabindex_query: Query<
159        'w,
160        's,
161        (Entity, Option<&'static TabIndex>, Option<&'static Children>),
162        Without<TabGroup>,
163    >,
164    // Query for parents.
165    parent_query: Query<'w, 's, &'static ChildOf>,
166}
167
168impl TabNavigation<'_, '_> {
169    /// Navigate to the desired focusable entity, relative to the current focused entity.
170    ///
171    /// Change the [`NavAction`] to navigate in a different direction.
172    /// Focusable entities are determined by the presence of the [`TabIndex`] component.
173    ///
174    /// If there is no currently focused entity, then this function will return either the first
175    /// or last focusable entity, depending on the direction of navigation. For example, if
176    /// `action` is `Next` and no focusable entities are found, then this function will return
177    /// the first focusable entity.
178    pub fn navigate(
179        &self,
180        focus: &InputFocus,
181        action: NavAction,
182    ) -> Result<Entity, TabNavigationError> {
183        // If there are no tab groups, then there are no focusable entities.
184        if self.tabgroup_query.is_empty() {
185            return Err(TabNavigationError::NoTabGroups);
186        }
187
188        // Start by identifying which tab group we are in. Mainly what we want to know is if
189        // we're in a modal group.
190        let tabgroup = focus.0.and_then(|focus_ent| {
191            self.parent_query
192                .iter_ancestors(focus_ent)
193                .find_map(|entity| {
194                    self.tabgroup_query
195                        .get(entity)
196                        .ok()
197                        .map(|(_, tg, _)| (entity, tg))
198                })
199        });
200
201        self.navigate_internal(focus.0, action, tabgroup)
202    }
203
204    /// Initialize focus to a focusable child of a container, either the first or last
205    /// depending on [`NavAction`]. This assumes that the parent entity has a [`TabGroup`]
206    /// component.
207    ///
208    /// Focusable entities are determined by the presence of the [`TabIndex`] component.
209    pub fn initialize(
210        &self,
211        parent: Entity,
212        action: NavAction,
213    ) -> Result<Entity, TabNavigationError> {
214        // If there are no tab groups, then there are no focusable entities.
215        if self.tabgroup_query.is_empty() {
216            return Err(TabNavigationError::NoTabGroups);
217        }
218
219        // Look for the tab group on the parent entity.
220        match self.tabgroup_query.get(parent) {
221            Ok(tabgroup) => self.navigate_internal(None, action, Some((parent, tabgroup.1))),
222            Err(_) => Err(TabNavigationError::NoTabGroups),
223        }
224    }
225
226    pub fn navigate_internal(
227        &self,
228        focus: Option<Entity>,
229        action: NavAction,
230        tabgroup: Option<(Entity, &TabGroup)>,
231    ) -> Result<Entity, TabNavigationError> {
232        let navigation_result = self.navigate_in_group(tabgroup, focus, action);
233
234        match navigation_result {
235            Ok(entity) => {
236                if let Some(previous_focus) = focus
237                    && tabgroup.is_none()
238                {
239                    Err(TabNavigationError::NoTabGroupForCurrentFocus {
240                        previous_focus,
241                        new_focus: entity,
242                    })
243                } else {
244                    Ok(entity)
245                }
246            }
247            Err(e) => Err(e),
248        }
249    }
250
251    fn navigate_in_group(
252        &self,
253        tabgroup: Option<(Entity, &TabGroup)>,
254        focus: Option<Entity>,
255        action: NavAction,
256    ) -> Result<Entity, TabNavigationError> {
257        // List of all focusable entities found.
258        let mut focusable: Vec<(Entity, TabIndex, usize)> =
259            Vec::with_capacity(self.tabindex_query.iter().len());
260
261        match tabgroup {
262            Some((tg_entity, tg)) if tg.modal => {
263                // We're in a modal tab group, then gather all tab indices in that group.
264                if let Ok((_, _, children)) = self.tabgroup_query.get(tg_entity) {
265                    for child in children.iter() {
266                        self.gather_focusable(&mut focusable, *child, 0);
267                    }
268                }
269            }
270            _ => {
271                // Otherwise, gather all tab indices in all non-modal tab groups.
272                let mut tab_groups: Vec<(Entity, TabGroup)> = self
273                    .tabgroup_query
274                    .iter()
275                    .filter(|(_, tg, _)| !tg.modal)
276                    .map(|(e, tg, _)| (e, *tg))
277                    .collect();
278                // Stable sort by group order
279                tab_groups.sort_by_key(|(_, tg)| tg.order);
280
281                // Search group descendants
282                tab_groups
283                    .iter()
284                    .enumerate()
285                    .for_each(|(idx, (tg_entity, _))| {
286                        self.gather_focusable(&mut focusable, *tg_entity, idx);
287                    });
288            }
289        }
290
291        if focusable.is_empty() {
292            return Err(TabNavigationError::NoFocusableEntities);
293        }
294
295        // Sort by TabGroup and then TabIndex
296        focusable.sort_by(|(_, a_tab_idx, a_group), (_, b_tab_idx, b_group)| {
297            if a_group == b_group {
298                a_tab_idx.cmp(b_tab_idx)
299            } else {
300                a_group.cmp(b_group)
301            }
302        });
303
304        let index = focusable.iter().position(|e| Some(e.0) == focus);
305        let count = focusable.len();
306        let next = match (index, action) {
307            (Some(idx), NavAction::Next) => (idx + 1).rem_euclid(count),
308            (Some(idx), NavAction::Previous) => (idx + count - 1).rem_euclid(count),
309            (None, NavAction::Next) | (_, NavAction::First) => 0,
310            (None, NavAction::Previous) | (_, NavAction::Last) => count - 1,
311        };
312        match focusable.get(next) {
313            Some((entity, _, _)) => Ok(*entity),
314            None => Err(TabNavigationError::FailedToNavigateToNextFocusableEntity),
315        }
316    }
317
318    /// Gather all focusable entities in tree order.
319    fn gather_focusable(
320        &self,
321        out: &mut Vec<(Entity, TabIndex, usize)>,
322        parent: Entity,
323        tab_group_idx: usize,
324    ) {
325        if let Ok((entity, tabindex, children)) = self.tabindex_query.get(parent) {
326            if let Some(tabindex) = tabindex {
327                if tabindex.0 >= 0 {
328                    out.push((entity, *tabindex, tab_group_idx));
329                }
330            }
331            if let Some(children) = children {
332                for child in children.iter() {
333                    // Don't traverse into tab groups, as they are handled separately.
334                    if self.tabgroup_query.get(*child).is_err() {
335                        self.gather_focusable(out, *child, tab_group_idx);
336                    }
337                }
338            }
339        } else if let Ok((_, tabgroup, children)) = self.tabgroup_query.get(parent) {
340            if !tabgroup.modal {
341                for child in children.iter() {
342                    self.gather_focusable(out, *child, tab_group_idx);
343                }
344            }
345        }
346    }
347}
348
349/// Observer which sets focus to the nearest ancestor that has tab index, using bubbling.
350pub(crate) fn acquire_focus(
351    mut acquire_focus: On<AcquireFocus>,
352    focusable: Query<(), With<TabIndex>>,
353    windows: Query<(), With<Window>>,
354    mut focus: ResMut<InputFocus>,
355) {
356    // If the entity has a TabIndex
357    if focusable.contains(acquire_focus.focused_entity) {
358        // Stop and focus it
359        acquire_focus.propagate(false);
360        // Don't mutate unless we need to, for change detection
361        if focus.0 != Some(acquire_focus.focused_entity) {
362            focus.0 = Some(acquire_focus.focused_entity);
363        }
364    } else if windows.contains(acquire_focus.focused_entity) {
365        // Stop and clear focus
366        acquire_focus.propagate(false);
367        // Don't mutate unless we need to, for change detection
368        if focus.0.is_some() {
369            focus.clear();
370        }
371    }
372}
373
374/// Plugin for navigating between focusable entities using keyboard input.
375pub struct TabNavigationPlugin;
376
377impl Plugin for TabNavigationPlugin {
378    fn build(&self, app: &mut App) {
379        app.add_systems(Startup, setup_tab_navigation);
380        app.add_observer(acquire_focus);
381        #[cfg(feature = "bevy_picking")]
382        app.add_observer(click_to_focus);
383    }
384}
385
386fn setup_tab_navigation(mut commands: Commands, window: Query<Entity, With<PrimaryWindow>>) {
387    for window in window.iter() {
388        commands.entity(window).observe(handle_tab_navigation);
389    }
390}
391
392#[cfg(feature = "bevy_picking")]
393fn click_to_focus(
394    press: On<bevy_picking::events::Pointer<bevy_picking::events::Press>>,
395    mut focus_visible: ResMut<InputFocusVisible>,
396    windows: Query<Entity, With<PrimaryWindow>>,
397    mut commands: Commands,
398) {
399    // Because `Pointer` is a bubbling event, we don't want to trigger an `AcquireFocus` event
400    // for every ancestor, but only for the original entity. Also, users may want to stop
401    // propagation on the pointer event at some point along the bubbling chain, so we need our
402    // own dedicated event whose propagation we can control.
403    if press.entity == press.original_event_target() {
404        // Clicking hides focus
405        if focus_visible.0 {
406            focus_visible.0 = false;
407        }
408        // Search for a focusable parent entity, defaulting to window if none.
409        if let Ok(window) = windows.single() {
410            commands.trigger(AcquireFocus {
411                focused_entity: press.entity,
412                window,
413            });
414        }
415    }
416}
417
418/// Observer function which handles tab navigation.
419///
420/// This observer responds to [`KeyCode::Tab`] events and Shift+Tab events,
421/// cycling through focusable entities in the order determined by their tab index.
422///
423/// Any [`TabNavigationError`]s that occur during tab navigation are logged as warnings.
424pub fn handle_tab_navigation(
425    mut event: On<FocusedInput<KeyboardInput>>,
426    nav: TabNavigation,
427    mut focus: ResMut<InputFocus>,
428    mut visible: ResMut<InputFocusVisible>,
429    keys: Res<ButtonInput<KeyCode>>,
430) {
431    // Tab navigation.
432    let key_event = &event.input;
433    if key_event.key_code == KeyCode::Tab
434        && key_event.state == ButtonState::Pressed
435        && !key_event.repeat
436    {
437        let maybe_next = nav.navigate(
438            &focus,
439            if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
440                NavAction::Previous
441            } else {
442                NavAction::Next
443            },
444        );
445
446        match maybe_next {
447            Ok(next) => {
448                event.propagate(false);
449                focus.set(next);
450                visible.0 = true;
451            }
452            Err(e) => {
453                warn!("Tab navigation error: {e}");
454                // This failure mode is recoverable, but still indicates a problem.
455                if let TabNavigationError::NoTabGroupForCurrentFocus { new_focus, .. } = e {
456                    event.propagate(false);
457                    focus.set(new_focus);
458                    visible.0 = true;
459                }
460            }
461        }
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use bevy_ecs::system::SystemState;
468
469    use super::*;
470
471    #[test]
472    fn test_tab_navigation() {
473        let mut app = App::new();
474        let world = app.world_mut();
475
476        let tab_group_entity = world.spawn(TabGroup::new(0)).id();
477        let tab_entity_1 = world.spawn((TabIndex(0), ChildOf(tab_group_entity))).id();
478        let tab_entity_2 = world.spawn((TabIndex(1), ChildOf(tab_group_entity))).id();
479
480        let mut system_state: SystemState<TabNavigation> = SystemState::new(world);
481        let tab_navigation = system_state.get(world);
482        assert_eq!(tab_navigation.tabgroup_query.iter().count(), 1);
483        assert!(tab_navigation.tabindex_query.iter().count() >= 2);
484
485        let next_entity =
486            tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next);
487        assert_eq!(next_entity, Ok(tab_entity_2));
488
489        let prev_entity =
490            tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous);
491        assert_eq!(prev_entity, Ok(tab_entity_1));
492
493        let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First);
494        assert_eq!(first_entity, Ok(tab_entity_1));
495
496        let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last);
497        assert_eq!(last_entity, Ok(tab_entity_2));
498    }
499
500    #[test]
501    fn test_tab_navigation_between_groups_is_sorted_by_group() {
502        let mut app = App::new();
503        let world = app.world_mut();
504
505        let tab_group_1 = world.spawn(TabGroup::new(0)).id();
506        let tab_entity_1 = world.spawn((TabIndex(0), ChildOf(tab_group_1))).id();
507        let tab_entity_2 = world.spawn((TabIndex(1), ChildOf(tab_group_1))).id();
508
509        let tab_group_2 = world.spawn(TabGroup::new(1)).id();
510        let tab_entity_3 = world.spawn((TabIndex(0), ChildOf(tab_group_2))).id();
511        let tab_entity_4 = world.spawn((TabIndex(1), ChildOf(tab_group_2))).id();
512
513        let mut system_state: SystemState<TabNavigation> = SystemState::new(world);
514        let tab_navigation = system_state.get(world);
515        assert_eq!(tab_navigation.tabgroup_query.iter().count(), 2);
516        assert!(tab_navigation.tabindex_query.iter().count() >= 4);
517
518        let next_entity =
519            tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next);
520        assert_eq!(next_entity, Ok(tab_entity_2));
521
522        let prev_entity =
523            tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous);
524        assert_eq!(prev_entity, Ok(tab_entity_1));
525
526        let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First);
527        assert_eq!(first_entity, Ok(tab_entity_1));
528
529        let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last);
530        assert_eq!(last_entity, Ok(tab_entity_4));
531
532        let next_from_end_of_group_entity =
533            tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Next);
534        assert_eq!(next_from_end_of_group_entity, Ok(tab_entity_3));
535
536        let prev_entity_from_start_of_group =
537            tab_navigation.navigate(&InputFocus::from_entity(tab_entity_3), NavAction::Previous);
538        assert_eq!(prev_entity_from_start_of_group, Ok(tab_entity_2));
539    }
540}