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