1use alloc::vec::Vec;
28use bevy_app::{App, Plugin, Startup};
29use bevy_camera::visibility::InheritedVisibility;
30use bevy_ecs::{
31 component::Component,
32 entity::Entity,
33 hierarchy::{ChildOf, Children},
34 observer::On,
35 query::{With, Without},
36 system::{Commands, Query, Res, ResMut, SystemParam},
37};
38use bevy_input::{
39 keyboard::{KeyCode, KeyboardInput},
40 ButtonInput, ButtonState,
41};
42use bevy_window::{PrimaryWindow, Window};
43use log::warn;
44use thiserror::Error;
45
46use crate::{AcquireFocus, FocusCause, FocusedInput, InputFocus, InputFocusVisible};
47
48#[cfg(feature = "bevy_reflect")]
49use {
50 bevy_ecs::prelude::ReflectComponent,
51 bevy_reflect::{prelude::*, Reflect},
52};
53
54#[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#[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 pub order: i32,
76
77 pub modal: bool,
82}
83
84impl TabGroup {
85 pub fn new(order: i32) -> Self {
87 Self {
88 order,
89 modal: false,
90 }
91 }
92
93 pub fn modal() -> Self {
95 Self {
96 order: 0,
97 modal: true,
98 }
99 }
100}
101
102#[derive(Default, Clone, Copy, Debug, PartialEq)]
106#[cfg_attr(
107 feature = "bevy_reflect",
108 derive(Reflect),
109 reflect(Debug, Clone, PartialEq)
110)]
111pub enum NavAction {
112 #[default]
116 Next,
117 Previous,
121 First,
125 Last,
129}
130
131#[derive(Debug, Error, PartialEq, Eq, Clone)]
133pub enum TabNavigationError {
134 #[error("No tab groups found")]
136 NoTabGroups,
137 #[error("No focusable entities found")]
139 NoFocusableEntities,
140 #[error("Failed to navigate to next focusable entity")]
144 FailedToNavigateToNextFocusableEntity,
145 #[error("No tab group found for currently focused entity {previous_focus}. Users will not be able to navigate back to this entity.")]
147 NoTabGroupForCurrentFocus {
148 previous_focus: Entity,
151 new_focus: Entity,
155 },
156}
157
158#[doc(hidden)]
160#[derive(SystemParam)]
161pub struct TabNavigation<'w, 's> {
162 tabgroup_query: Query<'w, 's, (Entity, &'static TabGroup, &'static Children)>,
164 tabindex_query: Query<
166 'w,
167 's,
168 (
169 Entity,
170 Option<&'static TabIndex>,
171 Option<&'static Children>,
172 Option<&'static InheritedVisibility>,
173 ),
174 Without<TabGroup>,
175 >,
176 parent_query: Query<'w, 's, &'static ChildOf>,
178}
179
180impl TabNavigation<'_, '_> {
181 pub fn navigate(
191 &self,
192 focus: &InputFocus,
193 action: NavAction,
194 ) -> Result<Entity, TabNavigationError> {
195 if self.tabgroup_query.is_empty() {
197 return Err(TabNavigationError::NoTabGroups);
198 }
199
200 let tabgroup = focus.get().and_then(|focus_ent| {
203 self.parent_query
204 .iter_ancestors(focus_ent)
205 .find_map(|entity| {
206 self.tabgroup_query
207 .get(entity)
208 .ok()
209 .map(|(_, tg, _)| (entity, tg))
210 })
211 });
212
213 self.navigate_internal(focus.get(), action, tabgroup)
214 }
215
216 pub fn initialize(
222 &self,
223 parent: Entity,
224 action: NavAction,
225 ) -> Result<Entity, TabNavigationError> {
226 if self.tabgroup_query.is_empty() {
228 return Err(TabNavigationError::NoTabGroups);
229 }
230
231 match self.tabgroup_query.get(parent) {
233 Ok(tabgroup) => self.navigate_internal(None, action, Some((parent, tabgroup.1))),
234 Err(_) => Err(TabNavigationError::NoTabGroups),
235 }
236 }
237
238 pub fn navigate_internal(
239 &self,
240 focus: Option<Entity>,
241 action: NavAction,
242 tabgroup: Option<(Entity, &TabGroup)>,
243 ) -> Result<Entity, TabNavigationError> {
244 let navigation_result = self.navigate_in_group(tabgroup, focus, action);
245
246 match navigation_result {
247 Ok(entity) => {
248 if let Some(previous_focus) = focus
249 && tabgroup.is_none()
250 {
251 Err(TabNavigationError::NoTabGroupForCurrentFocus {
252 previous_focus,
253 new_focus: entity,
254 })
255 } else {
256 Ok(entity)
257 }
258 }
259 Err(e) => Err(e),
260 }
261 }
262
263 fn navigate_in_group(
264 &self,
265 tabgroup: Option<(Entity, &TabGroup)>,
266 focus: Option<Entity>,
267 action: NavAction,
268 ) -> Result<Entity, TabNavigationError> {
269 let mut focusable: Vec<(Entity, TabIndex, usize)> =
271 Vec::with_capacity(self.tabindex_query.iter().len());
272
273 match tabgroup {
274 Some((tg_entity, tg)) if tg.modal => {
275 if let Ok((_, _, children)) = self.tabgroup_query.get(tg_entity) {
277 for child in children.iter() {
278 self.gather_focusable(&mut focusable, *child, 0);
279 }
280 }
281 }
282 _ => {
283 let mut tab_groups: Vec<(Entity, TabGroup)> = self
285 .tabgroup_query
286 .iter()
287 .filter(|(_, tg, _)| !tg.modal)
288 .map(|(e, tg, _)| (e, *tg))
289 .collect();
290 tab_groups.sort_by_key(|(_, tg)| tg.order);
292
293 tab_groups
295 .iter()
296 .enumerate()
297 .for_each(|(idx, (tg_entity, _))| {
298 self.gather_focusable(&mut focusable, *tg_entity, idx);
299 });
300 }
301 }
302
303 if focusable.is_empty() {
304 return Err(TabNavigationError::NoFocusableEntities);
305 }
306
307 focusable.sort_by(|(_, a_tab_idx, a_group), (_, b_tab_idx, b_group)| {
309 if a_group == b_group {
310 a_tab_idx.cmp(b_tab_idx)
311 } else {
312 a_group.cmp(b_group)
313 }
314 });
315
316 let index = focusable.iter().position(|e| Some(e.0) == focus);
317 let count = focusable.len();
318 let next = match (index, action) {
319 (Some(idx), NavAction::Next) => (idx + 1).rem_euclid(count),
320 (Some(idx), NavAction::Previous) => (idx + count - 1).rem_euclid(count),
321 (None, NavAction::Next) | (_, NavAction::First) => 0,
322 (None, NavAction::Previous) | (_, NavAction::Last) => count - 1,
323 };
324 match focusable.get(next) {
325 Some((entity, _, _)) => Ok(*entity),
326 None => Err(TabNavigationError::FailedToNavigateToNextFocusableEntity),
327 }
328 }
329
330 fn gather_focusable(
332 &self,
333 out: &mut Vec<(Entity, TabIndex, usize)>,
334 parent: Entity,
335 tab_group_idx: usize,
336 ) {
337 if let Ok((entity, tabindex, children, inherited_visibility)) =
338 self.tabindex_query.get(parent)
339 {
340 if inherited_visibility.is_none_or(|v| v.get()) {
343 if let Some(tabindex) = tabindex
344 && tabindex.0 >= 0
345 {
346 out.push((entity, *tabindex, tab_group_idx));
347 }
348 if let Some(children) = children {
349 for child in children.iter() {
350 if self.tabgroup_query.get(*child).is_err() {
352 self.gather_focusable(out, *child, tab_group_idx);
353 }
354 }
355 }
356 }
357 } else if let Ok((_, tabgroup, children)) = self.tabgroup_query.get(parent)
358 && !tabgroup.modal
359 {
360 for child in children.iter() {
361 self.gather_focusable(out, *child, tab_group_idx);
362 }
363 }
364 }
365}
366
367#[cfg_attr(
382 feature = "bevy_picking",
383 doc = "This observer is also registered by
384[`PointerFocusPlugin`](crate::pointer_focus::PointerFocusPlugin) as a temporary bridge so pointer
385clicks can acquire focus without [`TabNavigationPlugin`]. Because `add_observer` does not
386deduplicate, it may therefore run twice per request when both plugins are present; keep it
387idempotent (stop propagation, only mutate focus on a real change) so the second run is a no-op."
388)]
389pub fn acquire_focus_tab_index(
390 mut acquire_focus: On<AcquireFocus>,
391 focusable: Query<(), (With<TabIndex>, Without<Window>)>,
392 mut focus: ResMut<InputFocus>,
393) {
394 if focusable.contains(acquire_focus.focused_entity) {
396 acquire_focus.propagate(false);
398 if focus.get() != Some(acquire_focus.focused_entity) {
400 focus.set(acquire_focus.focused_entity, FocusCause::Navigated);
401 }
402 }
403}
404
405pub struct TabNavigationPlugin;
407
408impl Plugin for TabNavigationPlugin {
409 fn build(&self, app: &mut App) {
410 app.add_systems(Startup, setup_tab_navigation);
411 app.add_observer(acquire_focus_tab_index);
412 }
413}
414
415fn setup_tab_navigation(mut commands: Commands, window: Query<Entity, With<PrimaryWindow>>) {
416 for window in window.iter() {
417 commands.entity(window).observe(handle_tab_navigation);
418 }
419}
420
421pub fn handle_tab_navigation(
428 mut event: On<FocusedInput<KeyboardInput>>,
429 nav: TabNavigation,
430 mut focus: ResMut<InputFocus>,
431 mut visible: ResMut<InputFocusVisible>,
432 keys: Res<ButtonInput<KeyCode>>,
433) {
434 let key_event = &event.input;
436 if key_event.key_code == KeyCode::Tab
437 && key_event.state == ButtonState::Pressed
438 && !key_event.repeat
439 {
440 let maybe_next = nav.navigate(
441 &focus,
442 if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
443 NavAction::Previous
444 } else {
445 NavAction::Next
446 },
447 );
448
449 match maybe_next {
450 Ok(next) => {
451 event.propagate(false);
452 focus.set(next, FocusCause::Navigated);
453 visible.0 = true;
454 }
455 Err(e) => {
456 warn!("Tab navigation error: {e}");
457 if let TabNavigationError::NoTabGroupForCurrentFocus { new_focus, .. } = e {
459 event.propagate(false);
460 focus.set(new_focus, FocusCause::Navigated);
461 visible.0 = true;
462 }
463 }
464 }
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use bevy_ecs::system::SystemState;
471
472 use super::*;
473
474 #[test]
475 fn test_tab_navigation() {
476 let mut app = App::new();
477 let world = app.world_mut();
478
479 let tab_group_entity = world.spawn(TabGroup::new(0)).id();
480 let tab_entity_1 = world.spawn((TabIndex(0), ChildOf(tab_group_entity))).id();
481 let tab_entity_2 = world.spawn((TabIndex(1), ChildOf(tab_group_entity))).id();
482
483 let mut system_state: SystemState<TabNavigation> = SystemState::new(world);
484 let tab_navigation = system_state.get(world).unwrap();
485 assert_eq!(tab_navigation.tabgroup_query.iter().count(), 1);
486 assert!(tab_navigation.tabindex_query.iter().count() >= 2);
487
488 let next_entity =
489 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next);
490 assert_eq!(next_entity, Ok(tab_entity_2));
491
492 let prev_entity =
493 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous);
494 assert_eq!(prev_entity, Ok(tab_entity_1));
495
496 let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First);
497 assert_eq!(first_entity, Ok(tab_entity_1));
498
499 let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last);
500 assert_eq!(last_entity, Ok(tab_entity_2));
501 }
502
503 #[test]
504 fn test_tab_navigation_between_groups_is_sorted_by_group() {
505 let mut app = App::new();
506 let world = app.world_mut();
507
508 let tab_group_1 = world.spawn(TabGroup::new(0)).id();
509 let tab_entity_1 = world.spawn((TabIndex(0), ChildOf(tab_group_1))).id();
510 let tab_entity_2 = world.spawn((TabIndex(1), ChildOf(tab_group_1))).id();
511
512 let tab_group_2 = world.spawn(TabGroup::new(1)).id();
513 let tab_entity_3 = world.spawn((TabIndex(0), ChildOf(tab_group_2))).id();
514 let tab_entity_4 = world.spawn((TabIndex(1), ChildOf(tab_group_2))).id();
515
516 let mut system_state: SystemState<TabNavigation> = SystemState::new(world);
517 let tab_navigation = system_state.get(world).unwrap();
518 assert_eq!(tab_navigation.tabgroup_query.iter().count(), 2);
519 assert!(tab_navigation.tabindex_query.iter().count() >= 4);
520
521 let next_entity =
522 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next);
523 assert_eq!(next_entity, Ok(tab_entity_2));
524
525 let prev_entity =
526 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous);
527 assert_eq!(prev_entity, Ok(tab_entity_1));
528
529 let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First);
530 assert_eq!(first_entity, Ok(tab_entity_1));
531
532 let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last);
533 assert_eq!(last_entity, Ok(tab_entity_4));
534
535 let next_from_end_of_group_entity =
536 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Next);
537 assert_eq!(next_from_end_of_group_entity, Ok(tab_entity_3));
538
539 let prev_entity_from_start_of_group =
540 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_3), NavAction::Previous);
541 assert_eq!(prev_entity_from_start_of_group, Ok(tab_entity_2));
542 }
543
544 fn acquire_focus_app() -> (App, Entity) {
548 use crate::InputFocusPlugin;
549 use bevy_input::InputPlugin;
550
551 let mut app = App::new();
552 app.add_plugins((InputPlugin, InputFocusPlugin, TabNavigationPlugin));
553 let window = app
554 .world_mut()
555 .spawn((Window::default(), PrimaryWindow))
556 .id();
557 app.update();
559 (app, window)
560 }
561
562 #[test]
563 fn acquire_focus_focuses_entity_with_tab_index() {
564 let (mut app, window) = acquire_focus_app();
565
566 let focusable = app.world_mut().spawn(TabIndex(0)).id();
567
568 app.world_mut().trigger(AcquireFocus {
569 focused_entity: focusable,
570 window,
571 });
572 app.update();
573
574 assert_eq!(app.world().resource::<InputFocus>().get(), Some(focusable));
575 }
576
577 #[test]
578 fn acquire_focus_does_not_focus_entity_without_tab_index() {
579 let (mut app, window) = acquire_focus_app();
580
581 let non_focusable = app.world_mut().spawn(ChildOf(window)).id();
586 app.world_mut().trigger(AcquireFocus {
587 focused_entity: non_focusable,
588 window,
589 });
590 app.update();
591
592 assert_ne!(
593 app.world().resource::<InputFocus>().get(),
594 Some(non_focusable)
595 );
596 assert_eq!(app.world().resource::<InputFocus>().get(), None);
597 }
598}