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_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#[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(Clone, Copy)]
106pub enum NavAction {
107 Next,
111 Previous,
115 First,
119 Last,
123}
124
125#[derive(Debug, Error, PartialEq, Eq, Clone)]
127pub enum TabNavigationError {
128 #[error("No tab groups found")]
130 NoTabGroups,
131 #[error("No focusable entities found")]
133 NoFocusableEntities,
134 #[error("Failed to navigate to next focusable entity")]
138 FailedToNavigateToNextFocusableEntity,
139 #[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 previous_focus: Entity,
145 new_focus: Entity,
149 },
150}
151
152#[doc(hidden)]
154#[derive(SystemParam)]
155pub struct TabNavigation<'w, 's> {
156 tabgroup_query: Query<'w, 's, (Entity, &'static TabGroup, &'static Children)>,
158 tabindex_query: Query<
160 'w,
161 's,
162 (Entity, Option<&'static TabIndex>, Option<&'static Children>),
163 Without<TabGroup>,
164 >,
165 parent_query: Query<'w, 's, &'static ChildOf>,
167}
168
169impl TabNavigation<'_, '_> {
170 pub fn navigate(
180 &self,
181 focus: &InputFocus,
182 action: NavAction,
183 ) -> Result<Entity, TabNavigationError> {
184 if self.tabgroup_query.is_empty() {
186 return Err(TabNavigationError::NoTabGroups);
187 }
188
189 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 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 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 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 tab_groups.sort_by_key(|(_, tg)| tg.order);
248
249 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 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 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 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
317pub(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 focusable.contains(acquire_focus.focused_entity) {
326 acquire_focus.propagate(false);
328 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 acquire_focus.propagate(false);
335 if focus.0.is_some() {
337 focus.clear();
338 }
339 }
340}
341
342pub 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 if press.entity == press.original_event_target() {
370 if focus_visible.0 {
372 focus_visible.0 = false;
373 }
374 if let Ok(window) = windows.single() {
376 commands.trigger(AcquireFocus {
377 focused_entity: press.entity,
378 window,
379 });
380 }
381 }
382}
383
384pub 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 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 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}