Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_input_focus/src/tab_navigation.rs
6595 views
1
//! This module provides a framework for handling linear tab-key navigation in Bevy applications.
2
//!
3
//! The rules of tabbing are derived from the HTML specification, and are as follows:
4
//!
5
//! * An index >= 0 means that the entity is tabbable via sequential navigation.
6
//! The order of tabbing is determined by the index, with lower indices being tabbed first.
7
//! If two entities have the same index, then the order is determined by the order of
8
//! the entities in the ECS hierarchy (as determined by Parent/Child).
9
//! * An index < 0 means that the entity is not focusable via sequential navigation, but
10
//! can still be focused via direct selection.
11
//!
12
//! Tabbable entities must be descendants of a [`TabGroup`] entity, which is a component that
13
//! marks a tree of entities as containing tabbable elements. The order of tab groups
14
//! is determined by the [`TabGroup::order`] field, with lower orders being tabbed first. Modal tab groups
15
//! are used for ui elements that should only tab within themselves, such as modal dialog boxes.
16
//!
17
//! To enable automatic tabbing, add the
18
//! [`TabNavigationPlugin`] and [`InputDispatchPlugin`](crate::InputDispatchPlugin) to your app.
19
//! This will install a keyboard event observer on the primary window which automatically handles
20
//! tab navigation for you.
21
//!
22
//! Alternatively, if you want to have more control over tab navigation, or are using an input-action-mapping framework,
23
//! you can use the [`TabNavigation`] system parameter directly instead.
24
//! This object can be injected into your systems, and provides a [`navigate`](`TabNavigation::navigate`) method which can be
25
//! used to navigate between focusable entities.
26
27
use alloc::vec::Vec;
28
use bevy_app::{App, Plugin, Startup};
29
use 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
};
37
use bevy_input::{
38
keyboard::{KeyCode, KeyboardInput},
39
ButtonInput, ButtonState,
40
};
41
use bevy_picking::events::{Pointer, Press};
42
use bevy_window::{PrimaryWindow, Window};
43
use log::warn;
44
use thiserror::Error;
45
46
use crate::{AcquireFocus, FocusedInput, InputFocus, InputFocusVisible};
47
48
#[cfg(feature = "bevy_reflect")]
49
use {
50
bevy_ecs::prelude::ReflectComponent,
51
bevy_reflect::{prelude::*, Reflect},
52
};
53
54
/// A component which indicates that an entity wants to participate in tab navigation.
55
///
56
/// Note that you must also add the [`TabGroup`] component to the entity's ancestor in order
57
/// for this component to have any effect.
58
#[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
)]
64
pub struct TabIndex(pub i32);
65
66
/// A component used to mark a tree of entities as containing tabbable elements.
67
#[derive(Debug, Default, Component, Copy, Clone)]
68
#[cfg_attr(
69
feature = "bevy_reflect",
70
derive(Reflect),
71
reflect(Debug, Default, Component, Clone)
72
)]
73
pub struct TabGroup {
74
/// The order of the tab group relative to other tab groups.
75
pub order: i32,
76
77
/// Whether this is a 'modal' group. If true, then tabbing within the group (that is,
78
/// if the current focus entity is a child of this group) will cycle through the children
79
/// of this group. If false, then tabbing within the group will cycle through all non-modal
80
/// tab groups.
81
pub modal: bool,
82
}
83
84
impl TabGroup {
85
/// Create a new tab group with the given order.
86
pub fn new(order: i32) -> Self {
87
Self {
88
order,
89
modal: false,
90
}
91
}
92
93
/// Create a modal tab group.
94
pub fn modal() -> Self {
95
Self {
96
order: 0,
97
modal: true,
98
}
99
}
100
}
101
102
/// A navigation action that users might take to navigate your user interface in a cyclic fashion.
103
///
104
/// These values are consumed by the [`TabNavigation`] system param.
105
#[derive(Clone, Copy)]
106
pub enum NavAction {
107
/// Navigate to the next focusable entity, wrapping around to the beginning if at the end.
108
///
109
/// This is commonly triggered by pressing the Tab key.
110
Next,
111
/// Navigate to the previous focusable entity, wrapping around to the end if at the beginning.
112
///
113
/// This is commonly triggered by pressing Shift+Tab.
114
Previous,
115
/// Navigate to the first focusable entity.
116
///
117
/// This is commonly triggered by pressing Home.
118
First,
119
/// Navigate to the last focusable entity.
120
///
121
/// This is commonly triggered by pressing End.
122
Last,
123
}
124
125
/// An error that can occur during [tab navigation](crate::tab_navigation).
126
#[derive(Debug, Error, PartialEq, Eq, Clone)]
127
pub enum TabNavigationError {
128
/// No tab groups were found.
129
#[error("No tab groups found")]
130
NoTabGroups,
131
/// No focusable entities were found.
132
#[error("No focusable entities found")]
133
NoFocusableEntities,
134
/// Could not navigate to the next focusable entity.
135
///
136
/// This can occur if your tab groups are malformed.
137
#[error("Failed to navigate to next focusable entity")]
138
FailedToNavigateToNextFocusableEntity,
139
/// No tab group for the current focus entity was found.
140
#[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
/// The entity that was previously focused,
143
/// and is missing its tab group.
144
previous_focus: Entity,
145
/// The new entity that will be focused.
146
///
147
/// If you want to recover from this error, set [`InputFocus`] to this entity.
148
new_focus: Entity,
149
},
150
}
151
152
/// An injectable helper object that provides tab navigation functionality.
153
#[doc(hidden)]
154
#[derive(SystemParam)]
155
pub struct TabNavigation<'w, 's> {
156
// Query for tab groups.
157
tabgroup_query: Query<'w, 's, (Entity, &'static TabGroup, &'static Children)>,
158
// Query for tab indices.
159
tabindex_query: Query<
160
'w,
161
's,
162
(Entity, Option<&'static TabIndex>, Option<&'static Children>),
163
Without<TabGroup>,
164
>,
165
// Query for parents.
166
parent_query: Query<'w, 's, &'static ChildOf>,
167
}
168
169
impl TabNavigation<'_, '_> {
170
/// Navigate to the desired focusable entity.
171
///
172
/// Change the [`NavAction`] to navigate in a different direction.
173
/// Focusable entities are determined by the presence of the [`TabIndex`] component.
174
///
175
/// If no focusable entities are found, then this function will return either the first
176
/// or last focusable entity, depending on the direction of navigation. For example, if
177
/// `action` is `Next` and no focusable entities are found, then this function will return
178
/// the first focusable entity.
179
pub fn navigate(
180
&self,
181
focus: &InputFocus,
182
action: NavAction,
183
) -> Result<Entity, TabNavigationError> {
184
// If there are no tab groups, then there are no focusable entities.
185
if self.tabgroup_query.is_empty() {
186
return Err(TabNavigationError::NoTabGroups);
187
}
188
189
// Start by identifying which tab group we are in. Mainly what we want to know is if
190
// we're in a modal group.
191
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
// List of all focusable entities found.
226
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
// We're in a modal tab group, then gather all tab indices in that group.
232
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
// Otherwise, gather all tab indices in all non-modal tab groups.
240
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
// Stable sort by group order
247
tab_groups.sort_by_key(|(_, tg)| tg.order);
248
249
// Search group descendants
250
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
// Sort by TabGroup and then TabIndex
264
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
/// Gather all focusable entities in tree order.
287
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
// Don't traverse into tab groups, as they are handled separately.
302
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
317
/// Observer which sets focus to the nearest ancestor that has tab index, using bubbling.
318
pub(crate) fn acquire_focus(
319
mut ev: On<AcquireFocus>,
320
focusable: Query<(), With<TabIndex>>,
321
windows: Query<(), With<Window>>,
322
mut focus: ResMut<InputFocus>,
323
) {
324
// If the entity has a TabIndex
325
if focusable.contains(ev.entity()) {
326
// Stop and focus it
327
ev.propagate(false);
328
// Don't mutate unless we need to, for change detection
329
if focus.0 != Some(ev.entity()) {
330
focus.0 = Some(ev.entity());
331
}
332
} else if windows.contains(ev.entity()) {
333
// Stop and clear focus
334
ev.propagate(false);
335
// Don't mutate unless we need to, for change detection
336
if focus.0.is_some() {
337
focus.clear();
338
}
339
}
340
}
341
342
/// Plugin for navigating between focusable entities using keyboard input.
343
pub struct TabNavigationPlugin;
344
345
impl 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
353
fn 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
359
fn click_to_focus(
360
ev: On<Pointer<Press>>,
361
mut focus_visible: ResMut<InputFocusVisible>,
362
windows: Query<Entity, With<PrimaryWindow>>,
363
mut commands: Commands,
364
) {
365
// Because `Pointer` is a bubbling event, we don't want to trigger an `AcquireFocus` event
366
// for every ancestor, but only for the original entity. Also, users may want to stop
367
// propagation on the pointer event at some point along the bubbling chain, so we need our
368
// own dedicated event whose propagation we can control.
369
if ev.entity() == ev.original_entity() {
370
// Clicking hides focus
371
if focus_visible.0 {
372
focus_visible.0 = false;
373
}
374
// Search for a focusable parent entity, defaulting to window if none.
375
if let Ok(window) = windows.single() {
376
commands
377
.entity(ev.entity())
378
.trigger(AcquireFocus { window });
379
}
380
}
381
}
382
383
/// Observer function which handles tab navigation.
384
///
385
/// This observer responds to [`KeyCode::Tab`] events and Shift+Tab events,
386
/// cycling through focusable entities in the order determined by their tab index.
387
///
388
/// Any [`TabNavigationError`]s that occur during tab navigation are logged as warnings.
389
pub fn handle_tab_navigation(
390
mut event: On<FocusedInput<KeyboardInput>>,
391
nav: TabNavigation,
392
mut focus: ResMut<InputFocus>,
393
mut visible: ResMut<InputFocusVisible>,
394
keys: Res<ButtonInput<KeyCode>>,
395
) {
396
// Tab navigation.
397
let key_event = &event.input;
398
if key_event.key_code == KeyCode::Tab
399
&& key_event.state == ButtonState::Pressed
400
&& !key_event.repeat
401
{
402
let maybe_next = nav.navigate(
403
&focus,
404
if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
405
NavAction::Previous
406
} else {
407
NavAction::Next
408
},
409
);
410
411
match maybe_next {
412
Ok(next) => {
413
event.propagate(false);
414
focus.set(next);
415
visible.0 = true;
416
}
417
Err(e) => {
418
warn!("Tab navigation error: {e}");
419
// This failure mode is recoverable, but still indicates a problem.
420
if let TabNavigationError::NoTabGroupForCurrentFocus { new_focus, .. } = e {
421
event.propagate(false);
422
focus.set(new_focus);
423
visible.0 = true;
424
}
425
}
426
}
427
}
428
}
429
430
#[cfg(test)]
431
mod tests {
432
use bevy_ecs::system::SystemState;
433
434
use super::*;
435
436
#[test]
437
fn test_tab_navigation() {
438
let mut app = App::new();
439
let world = app.world_mut();
440
441
let tab_group_entity = world.spawn(TabGroup::new(0)).id();
442
let tab_entity_1 = world.spawn((TabIndex(0), ChildOf(tab_group_entity))).id();
443
let tab_entity_2 = world.spawn((TabIndex(1), ChildOf(tab_group_entity))).id();
444
445
let mut system_state: SystemState<TabNavigation> = SystemState::new(world);
446
let tab_navigation = system_state.get(world);
447
assert_eq!(tab_navigation.tabgroup_query.iter().count(), 1);
448
assert_eq!(tab_navigation.tabindex_query.iter().count(), 2);
449
450
let next_entity =
451
tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next);
452
assert_eq!(next_entity, Ok(tab_entity_2));
453
454
let prev_entity =
455
tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous);
456
assert_eq!(prev_entity, Ok(tab_entity_1));
457
458
let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First);
459
assert_eq!(first_entity, Ok(tab_entity_1));
460
461
let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last);
462
assert_eq!(last_entity, Ok(tab_entity_2));
463
}
464
465
#[test]
466
fn test_tab_navigation_between_groups_is_sorted_by_group() {
467
let mut app = App::new();
468
let world = app.world_mut();
469
470
let tab_group_1 = world.spawn(TabGroup::new(0)).id();
471
let tab_entity_1 = world.spawn((TabIndex(0), ChildOf(tab_group_1))).id();
472
let tab_entity_2 = world.spawn((TabIndex(1), ChildOf(tab_group_1))).id();
473
474
let tab_group_2 = world.spawn(TabGroup::new(1)).id();
475
let tab_entity_3 = world.spawn((TabIndex(0), ChildOf(tab_group_2))).id();
476
let tab_entity_4 = world.spawn((TabIndex(1), ChildOf(tab_group_2))).id();
477
478
let mut system_state: SystemState<TabNavigation> = SystemState::new(world);
479
let tab_navigation = system_state.get(world);
480
assert_eq!(tab_navigation.tabgroup_query.iter().count(), 2);
481
assert_eq!(tab_navigation.tabindex_query.iter().count(), 4);
482
483
let next_entity =
484
tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next);
485
assert_eq!(next_entity, Ok(tab_entity_2));
486
487
let prev_entity =
488
tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous);
489
assert_eq!(prev_entity, Ok(tab_entity_1));
490
491
let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First);
492
assert_eq!(first_entity, Ok(tab_entity_1));
493
494
let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last);
495
assert_eq!(last_entity, Ok(tab_entity_4));
496
497
let next_from_end_of_group_entity =
498
tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Next);
499
assert_eq!(next_from_end_of_group_entity, Ok(tab_entity_3));
500
501
let prev_entity_from_start_of_group =
502
tab_navigation.navigate(&InputFocus::from_entity(tab_entity_3), NavAction::Previous);
503
assert_eq!(prev_entity_from_start_of_group, Ok(tab_entity_2));
504
}
505
}
506
507