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::Trigger,
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;
42use log::warn;
43use thiserror::Error;
44
45use crate::{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
101pub enum NavAction {
105 Next,
109 Previous,
113 First,
117 Last,
121}
122
123#[derive(Debug, Error, PartialEq, Eq, Clone)]
125pub enum TabNavigationError {
126 #[error("No tab groups found")]
128 NoTabGroups,
129 #[error("No focusable entities found")]
131 NoFocusableEntities,
132 #[error("Failed to navigate to next focusable entity")]
136 FailedToNavigateToNextFocusableEntity,
137 #[error("No tab group found for currently focused entity {previous_focus}. Users will not be able to navigate back to this entity.")]
139 NoTabGroupForCurrentFocus {
140 previous_focus: Entity,
143 new_focus: Entity,
147 },
148}
149
150#[doc(hidden)]
152#[derive(SystemParam)]
153pub struct TabNavigation<'w, 's> {
154 tabgroup_query: Query<'w, 's, (Entity, &'static TabGroup, &'static Children)>,
156 tabindex_query: Query<
158 'w,
159 's,
160 (Entity, Option<&'static TabIndex>, Option<&'static Children>),
161 Without<TabGroup>,
162 >,
163 parent_query: Query<'w, 's, &'static ChildOf>,
165}
166
167impl TabNavigation<'_, '_> {
168 pub fn navigate(
178 &self,
179 focus: &InputFocus,
180 action: NavAction,
181 ) -> Result<Entity, TabNavigationError> {
182 if self.tabgroup_query.is_empty() {
184 return Err(TabNavigationError::NoTabGroups);
185 }
186
187 let tabgroup = focus.0.and_then(|focus_ent| {
190 self.parent_query
191 .iter_ancestors(focus_ent)
192 .find_map(|entity| {
193 self.tabgroup_query
194 .get(entity)
195 .ok()
196 .map(|(_, tg, _)| (entity, tg))
197 })
198 });
199
200 let navigation_result = self.navigate_in_group(tabgroup, focus, action);
201
202 match navigation_result {
203 Ok(entity) => {
204 if focus.0.is_some() && tabgroup.is_none() {
205 Err(TabNavigationError::NoTabGroupForCurrentFocus {
206 previous_focus: focus.0.unwrap(),
207 new_focus: entity,
208 })
209 } else {
210 Ok(entity)
211 }
212 }
213 Err(e) => Err(e),
214 }
215 }
216
217 fn navigate_in_group(
218 &self,
219 tabgroup: Option<(Entity, &TabGroup)>,
220 focus: &InputFocus,
221 action: NavAction,
222 ) -> Result<Entity, TabNavigationError> {
223 let mut focusable: Vec<(Entity, TabIndex)> =
225 Vec::with_capacity(self.tabindex_query.iter().len());
226
227 match tabgroup {
228 Some((tg_entity, tg)) if tg.modal => {
229 if let Ok((_, _, children)) = self.tabgroup_query.get(tg_entity) {
231 for child in children.iter() {
232 self.gather_focusable(&mut focusable, *child);
233 }
234 }
235 }
236 _ => {
237 let mut tab_groups: Vec<(Entity, TabGroup)> = self
239 .tabgroup_query
240 .iter()
241 .filter(|(_, tg, _)| !tg.modal)
242 .map(|(e, tg, _)| (e, *tg))
243 .collect();
244 tab_groups.sort_by_key(|(_, tg)| tg.order);
246
247 tab_groups.iter().for_each(|(tg_entity, _)| {
249 self.gather_focusable(&mut focusable, *tg_entity);
250 });
251 }
252 }
253
254 if focusable.is_empty() {
255 return Err(TabNavigationError::NoFocusableEntities);
256 }
257
258 focusable.sort_by_key(|(_, idx)| *idx);
260
261 let index = focusable.iter().position(|e| Some(e.0) == focus.0);
262 let count = focusable.len();
263 let next = match (index, action) {
264 (Some(idx), NavAction::Next) => (idx + 1).rem_euclid(count),
265 (Some(idx), NavAction::Previous) => (idx + count - 1).rem_euclid(count),
266 (None, NavAction::Next) | (_, NavAction::First) => 0,
267 (None, NavAction::Previous) | (_, NavAction::Last) => count - 1,
268 };
269 match focusable.get(next) {
270 Some((entity, _)) => Ok(*entity),
271 None => Err(TabNavigationError::FailedToNavigateToNextFocusableEntity),
272 }
273 }
274
275 fn gather_focusable(&self, out: &mut Vec<(Entity, TabIndex)>, parent: Entity) {
277 if let Ok((entity, tabindex, children)) = self.tabindex_query.get(parent) {
278 if let Some(tabindex) = tabindex {
279 if tabindex.0 >= 0 {
280 out.push((entity, *tabindex));
281 }
282 }
283 if let Some(children) = children {
284 for child in children.iter() {
285 if self.tabgroup_query.get(*child).is_err() {
287 self.gather_focusable(out, *child);
288 }
289 }
290 }
291 } else if let Ok((_, tabgroup, children)) = self.tabgroup_query.get(parent) {
292 if !tabgroup.modal {
293 for child in children.iter() {
294 self.gather_focusable(out, *child);
295 }
296 }
297 }
298 }
299}
300
301pub struct TabNavigationPlugin;
303
304impl Plugin for TabNavigationPlugin {
305 fn build(&self, app: &mut App) {
306 app.add_systems(Startup, setup_tab_navigation);
307
308 #[cfg(feature = "bevy_reflect")]
309 app.register_type::<TabIndex>().register_type::<TabGroup>();
310 }
311}
312
313fn setup_tab_navigation(mut commands: Commands, window: Query<Entity, With<PrimaryWindow>>) {
314 for window in window.iter() {
315 commands.entity(window).observe(handle_tab_navigation);
316 }
317}
318
319pub fn handle_tab_navigation(
326 mut trigger: Trigger<FocusedInput<KeyboardInput>>,
327 nav: TabNavigation,
328 mut focus: ResMut<InputFocus>,
329 mut visible: ResMut<InputFocusVisible>,
330 keys: Res<ButtonInput<KeyCode>>,
331) {
332 let key_event = &trigger.event().input;
334 if key_event.key_code == KeyCode::Tab
335 && key_event.state == ButtonState::Pressed
336 && !key_event.repeat
337 {
338 let maybe_next = nav.navigate(
339 &focus,
340 if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
341 NavAction::Previous
342 } else {
343 NavAction::Next
344 },
345 );
346
347 match maybe_next {
348 Ok(next) => {
349 trigger.propagate(false);
350 focus.set(next);
351 visible.0 = true;
352 }
353 Err(e) => {
354 warn!("Tab navigation error: {}", e);
355 if let TabNavigationError::NoTabGroupForCurrentFocus { new_focus, .. } = e {
357 trigger.propagate(false);
358 focus.set(new_focus);
359 visible.0 = true;
360 }
361 }
362 }
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use bevy_ecs::system::SystemState;
369
370 use super::*;
371
372 #[test]
373 fn test_tab_navigation() {
374 let mut app = App::new();
375 let world = app.world_mut();
376
377 let tab_group_entity = world.spawn(TabGroup::new(0)).id();
378 let tab_entity_1 = world.spawn((TabIndex(0), ChildOf(tab_group_entity))).id();
379 let tab_entity_2 = world.spawn((TabIndex(1), ChildOf(tab_group_entity))).id();
380
381 let mut system_state: SystemState<TabNavigation> = SystemState::new(world);
382 let tab_navigation = system_state.get(world);
383 assert_eq!(tab_navigation.tabgroup_query.iter().count(), 1);
384 assert_eq!(tab_navigation.tabindex_query.iter().count(), 2);
385
386 let next_entity =
387 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next);
388 assert_eq!(next_entity, Ok(tab_entity_2));
389
390 let prev_entity =
391 tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous);
392 assert_eq!(prev_entity, Ok(tab_entity_1));
393
394 let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First);
395 assert_eq!(first_entity, Ok(tab_entity_1));
396
397 let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last);
398 assert_eq!(last_entity, Ok(tab_entity_2));
399 }
400}