1use 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, FocusCause, FocusedInput, InputFocus, InputFocusVisible};
46
47#[cfg(feature = "bevy_reflect")]
48use {
49 bevy_ecs::prelude::ReflectComponent,
50 bevy_reflect::{prelude::*, Reflect},
51};
52
53#[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#[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 pub order: i32,
75
76 pub modal: bool,
81}
82
83impl TabGroup {
84 pub fn new(order: i32) -> Self {
86 Self {
87 order,
88 modal: false,
89 }
90 }
91
92 pub fn modal() -> Self {
94 Self {
95 order: 0,
96 modal: true,
97 }
98 }
99}
100
101#[derive(Clone, Copy, Debug, PartialEq)]
105#[cfg_attr(
106 feature = "bevy_reflect",
107 derive(Reflect),
108 reflect(Debug, Clone, PartialEq)
109)]
110pub enum NavAction {
111 Next,
115 Previous,
119 First,
123 Last,
127}
128
129#[derive(Debug, Error, PartialEq, Eq, Clone)]
131pub enum TabNavigationError {
132 #[error("No tab groups found")]
134 NoTabGroups,
135 #[error("No focusable entities found")]
137 NoFocusableEntities,
138 #[error("Failed to navigate to next focusable entity")]
142 FailedToNavigateToNextFocusableEntity,
143 #[error("No tab group found for currently focused entity {previous_focus}. Users will not be able to navigate back to this entity.")]
145 NoTabGroupForCurrentFocus {
146 previous_focus: Entity,
149 new_focus: Entity,
153 },
154}
155
156#[doc(hidden)]
158#[derive(SystemParam)]
159pub struct TabNavigation<'w, 's> {
160 tabgroup_query: Query<'w, 's, (Entity, &'static TabGroup, &'static Children)>,
162 tabindex_query: Query<
164 'w,
165 's,
166 (Entity, Option<&'static TabIndex>, Option<&'static Children>),
167 Without<TabGroup>,
168 >,
169 parent_query: Query<'w, 's, &'static ChildOf>,
171}
172
173impl TabNavigation<'_, '_> {
174 pub fn navigate(
184 &self,
185 focus: &InputFocus,
186 action: NavAction,
187 ) -> Result<Entity, TabNavigationError> {
188 if self.tabgroup_query.is_empty() {
190 return Err(TabNavigationError::NoTabGroups);
191 }
192
193 let tabgroup = focus.get().and_then(|focus_ent| {
196 self.parent_query
197 .iter_ancestors(focus_ent)
198 .find_map(|entity| {
199 self.tabgroup_query
200 .get(entity)
201 .ok()
202 .map(|(_, tg, _)| (entity, tg))
203 })
204 });
205
206 self.navigate_internal(focus.get(), action, tabgroup)
207 }
208
209 pub fn initialize(
215 &self,
216 parent: Entity,
217 action: NavAction,
218 ) -> Result<Entity, TabNavigationError> {
219 if self.tabgroup_query.is_empty() {
221 return Err(TabNavigationError::NoTabGroups);
222 }
223
224 match self.tabgroup_query.get(parent) {
226 Ok(tabgroup) => self.navigate_internal(None, action, Some((parent, tabgroup.1))),
227 Err(_) => Err(TabNavigationError::NoTabGroups),
228 }
229 }
230
231 pub fn navigate_internal(
232 &self,
233 focus: Option<Entity>,
234 action: NavAction,
235 tabgroup: Option<(Entity, &TabGroup)>,
236 ) -> Result<Entity, TabNavigationError> {
237 let navigation_result = self.navigate_in_group(tabgroup, focus, action);
238
239 match navigation_result {
240 Ok(entity) => {
241 if let Some(previous_focus) = focus
242 && tabgroup.is_none()
243 {
244 Err(TabNavigationError::NoTabGroupForCurrentFocus {
245 previous_focus,
246 new_focus: entity,
247 })
248 } else {
249 Ok(entity)
250 }
251 }
252 Err(e) => Err(e),
253 }
254 }
255
256 fn navigate_in_group(
257 &self,
258 tabgroup: Option<(Entity, &TabGroup)>,
259 focus: Option<Entity>,
260 action: NavAction,
261 ) -> Result<Entity, TabNavigationError> {
262 let mut focusable: Vec<(Entity, TabIndex, usize)> =
264 Vec::with_capacity(self.tabindex_query.iter().len());
265
266 match tabgroup {
267 Some((tg_entity, tg)) if tg.modal => {
268 if let Ok((_, _, children)) = self.tabgroup_query.get(tg_entity) {
270 for child in children.iter() {
271 self.gather_focusable(&mut focusable, *child, 0);
272 }
273 }
274 }
275 _ => {
276 let mut tab_groups: Vec<(Entity, TabGroup)> = self
278 .tabgroup_query
279 .iter()
280 .filter(|(_, tg, _)| !tg.modal)
281 .map(|(e, tg, _)| (e, *tg))
282 .collect();
283 tab_groups.sort_by_key(|(_, tg)| tg.order);
285
286 tab_groups
288 .iter()
289 .enumerate()
290 .for_each(|(idx, (tg_entity, _))| {
291 self.gather_focusable(&mut focusable, *tg_entity, idx);
292 });
293 }
294 }
295
296 if focusable.is_empty() {
297 return Err(TabNavigationError::NoFocusableEntities);
298 }
299
300 focusable.sort_by(|(_, a_tab_idx, a_group), (_, b_tab_idx, b_group)| {
302 if a_group == b_group {
303 a_tab_idx.cmp(b_tab_idx)
304 } else {
305 a_group.cmp(b_group)
306 }
307 });
308
309 let index = focusable.iter().position(|e| Some(e.0) == focus);
310 let count = focusable.len();
311 let next = match (index, action) {
312 (Some(idx), NavAction::Next) => (idx + 1).rem_euclid(count),
313 (Some(idx), NavAction::Previous) => (idx + count - 1).rem_euclid(count),
314 (None, NavAction::Next) | (_, NavAction::First) => 0,
315 (None, NavAction::Previous) | (_, NavAction::Last) => count - 1,
316 };
317 match focusable.get(next) {
318 Some((entity, _, _)) => Ok(*entity),
319 None => Err(TabNavigationError::FailedToNavigateToNextFocusableEntity),
320 }
321 }
322
323 fn gather_focusable(
325 &self,
326 out: &mut Vec<(Entity, TabIndex, usize)>,
327 parent: Entity,
328 tab_group_idx: usize,
329 ) {
330 if let Ok((entity, tabindex, children)) = self.tabindex_query.get(parent) {
331 if let Some(tabindex) = tabindex
332 && tabindex.0 >= 0
333 {
334 out.push((entity, *tabindex, tab_group_idx));
335 }
336 if let Some(children) = children {
337 for child in children.iter() {
338 if self.tabgroup_query.get(*child).is_err() {
340 self.gather_focusable(out, *child, tab_group_idx);
341 }
342 }
343 }
344 } else if let Ok((_, tabgroup, children)) = self.tabgroup_query.get(parent)
345 && !tabgroup.modal
346 {
347 for child in children.iter() {
348 self.gather_focusable(out, *child, tab_group_idx);
349 }
350 }
351 }
352}
353
354pub(crate) fn acquire_focus(
356 mut acquire_focus: On<AcquireFocus>,
357 focusable: Query<(), With<TabIndex>>,
358 windows: Query<(), With<Window>>,
359 mut focus: ResMut<InputFocus>,
360) {
361 if focusable.contains(acquire_focus.focused_entity) {
363 acquire_focus.propagate(false);
365 if focus.get() != Some(acquire_focus.focused_entity) {
367 focus.set(acquire_focus.focused_entity, FocusCause::Navigated);
368 }
369 } else if windows.contains(acquire_focus.focused_entity) {
370 acquire_focus.propagate(false);
372 if focus.get().is_some() {
374 focus.clear();
375 }
376 }
377}
378
379pub struct TabNavigationPlugin;
381
382impl Plugin for TabNavigationPlugin {
383 fn build(&self, app: &mut App) {
384 app.add_systems(Startup, setup_tab_navigation);
385 app.add_observer(acquire_focus);
386 #[cfg(feature = "bevy_picking")]
387 app.add_observer(click_to_focus);
388 }
389}
390
391fn setup_tab_navigation(mut commands: Commands, window: Query<Entity, With<PrimaryWindow>>) {
392 for window in window.iter() {
393 commands.entity(window).observe(handle_tab_navigation);
394 }
395}
396
397#[cfg(feature = "bevy_picking")]
398fn click_to_focus(
399 press: On<bevy_picking::events::Pointer<bevy_picking::events::Press>>,
400 mut focus_visible: ResMut<InputFocusVisible>,
401 windows: Query<Entity, With<PrimaryWindow>>,
402 mut commands: Commands,
403) {
404 if press.entity == press.original_event_target() {
409 if focus_visible.0 {
411 focus_visible.0 = false;
412 }
413 if let Ok(window) = windows.single() {
415 commands.trigger(AcquireFocus {
416 focused_entity: press.entity,
417 window,
418 });
419 }
420 }
421}
422
423pub fn handle_tab_navigation(
430 mut event: On<FocusedInput<KeyboardInput>>,
431 nav: TabNavigation,
432 mut focus: ResMut<InputFocus>,
433 mut visible: ResMut<InputFocusVisible>,
434 keys: Res<ButtonInput<KeyCode>>,
435) {
436 let key_event = &event.input;
438 if key_event.key_code == KeyCode::Tab
439 && key_event.state == ButtonState::Pressed
440 && !key_event.repeat
441 {
442 let maybe_next = nav.navigate(
443 &focus,
444 if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
445 NavAction::Previous
446 } else {
447 NavAction::Next
448 },
449 );
450
451 match maybe_next {
452 Ok(next) => {
453 event.propagate(false);
454 focus.set(next, FocusCause::Navigated);
455 visible.0 = true;
456 }
457 Err(e) => {
458 warn!("Tab navigation error: {e}");
459 if let TabNavigationError::NoTabGroupForCurrentFocus { new_focus, .. } = e {
461 event.propagate(false);
462 focus.set(new_focus, FocusCause::Navigated);
463 visible.0 = true;
464 }
465 }
466 }
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use bevy_ecs::system::SystemState;
473
474 use super::*;
475
476 #[test]
477 fn test_tab_navigation() {
478 let mut app = App::new();
479 let world = app.world_mut();
480
481 let tab_group_entity = world.spawn(TabGroup::new(0)).id();
482 let tab_entity_1 = world.spawn((TabIndex(0), ChildOf(tab_group_entity))).id();
483 let tab_entity_2 = world.spawn((TabIndex(1), ChildOf(tab_group_entity))).id();
484
485 let mut system_state: SystemState<TabNavigation> = SystemState::new(world);
486 let tab_navigation = system_state.get(world).unwrap();
487 assert_eq!(tab_navigation.tabgroup_query.iter().count(), 1);
488 assert!(tab_navigation.tabindex_query.iter().count() >= 2);
489
490 let next_entity =
491 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next);
492 assert_eq!(next_entity, Ok(tab_entity_2));
493
494 let prev_entity =
495 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous);
496 assert_eq!(prev_entity, Ok(tab_entity_1));
497
498 let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First);
499 assert_eq!(first_entity, Ok(tab_entity_1));
500
501 let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last);
502 assert_eq!(last_entity, Ok(tab_entity_2));
503 }
504
505 #[test]
506 fn test_tab_navigation_between_groups_is_sorted_by_group() {
507 let mut app = App::new();
508 let world = app.world_mut();
509
510 let tab_group_1 = world.spawn(TabGroup::new(0)).id();
511 let tab_entity_1 = world.spawn((TabIndex(0), ChildOf(tab_group_1))).id();
512 let tab_entity_2 = world.spawn((TabIndex(1), ChildOf(tab_group_1))).id();
513
514 let tab_group_2 = world.spawn(TabGroup::new(1)).id();
515 let tab_entity_3 = world.spawn((TabIndex(0), ChildOf(tab_group_2))).id();
516 let tab_entity_4 = world.spawn((TabIndex(1), ChildOf(tab_group_2))).id();
517
518 let mut system_state: SystemState<TabNavigation> = SystemState::new(world);
519 let tab_navigation = system_state.get(world).unwrap();
520 assert_eq!(tab_navigation.tabgroup_query.iter().count(), 2);
521 assert!(tab_navigation.tabindex_query.iter().count() >= 4);
522
523 let next_entity =
524 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next);
525 assert_eq!(next_entity, Ok(tab_entity_2));
526
527 let prev_entity =
528 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous);
529 assert_eq!(prev_entity, Ok(tab_entity_1));
530
531 let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First);
532 assert_eq!(first_entity, Ok(tab_entity_1));
533
534 let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last);
535 assert_eq!(last_entity, Ok(tab_entity_4));
536
537 let next_from_end_of_group_entity =
538 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Next);
539 assert_eq!(next_from_end_of_group_entity, Ok(tab_entity_3));
540
541 let prev_entity_from_start_of_group =
542 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_3), NavAction::Previous);
543 assert_eq!(prev_entity_from_start_of_group, Ok(tab_entity_2));
544 }
545}