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, 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)]
105pub enum NavAction {
106 Next,
110 Previous,
114 First,
118 Last,
122}
123
124#[derive(Debug, Error, PartialEq, Eq, Clone)]
126pub enum TabNavigationError {
127 #[error("No tab groups found")]
129 NoTabGroups,
130 #[error("No focusable entities found")]
132 NoFocusableEntities,
133 #[error("Failed to navigate to next focusable entity")]
137 FailedToNavigateToNextFocusableEntity,
138 #[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 previous_focus: Entity,
144 new_focus: Entity,
148 },
149}
150
151#[doc(hidden)]
153#[derive(SystemParam)]
154pub struct TabNavigation<'w, 's> {
155 tabgroup_query: Query<'w, 's, (Entity, &'static TabGroup, &'static Children)>,
157 tabindex_query: Query<
159 'w,
160 's,
161 (Entity, Option<&'static TabIndex>, Option<&'static Children>),
162 Without<TabGroup>,
163 >,
164 parent_query: Query<'w, 's, &'static ChildOf>,
166}
167
168impl TabNavigation<'_, '_> {
169 pub fn navigate(
179 &self,
180 focus: &InputFocus,
181 action: NavAction,
182 ) -> Result<Entity, TabNavigationError> {
183 if self.tabgroup_query.is_empty() {
185 return Err(TabNavigationError::NoTabGroups);
186 }
187
188 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 pub fn initialize(
210 &self,
211 parent: Entity,
212 action: NavAction,
213 ) -> Result<Entity, TabNavigationError> {
214 if self.tabgroup_query.is_empty() {
216 return Err(TabNavigationError::NoTabGroups);
217 }
218
219 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 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 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 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 tab_groups.sort_by_key(|(_, tg)| tg.order);
280
281 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 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 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 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
349pub(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 focusable.contains(acquire_focus.focused_entity) {
358 acquire_focus.propagate(false);
360 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 acquire_focus.propagate(false);
367 if focus.0.is_some() {
369 focus.clear();
370 }
371 }
372}
373
374pub 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 if press.entity == press.original_event_target() {
404 if focus_visible.0 {
406 focus_visible.0 = false;
407 }
408 if let Ok(window) = windows.single() {
410 commands.trigger(AcquireFocus {
411 focused_entity: press.entity,
412 window,
413 });
414 }
415 }
416}
417
418pub 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 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 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}