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
9353 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_window::{PrimaryWindow, Window};
42
use log::warn;
43
use thiserror::Error;
44
45
use crate::{AcquireFocus, FocusedInput, InputFocus, InputFocusVisible};
46
47
#[cfg(feature = "bevy_reflect")]
48
use {
49
bevy_ecs::prelude::ReflectComponent,
50
bevy_reflect::{prelude::*, Reflect},
51
};
52
53
/// A component which indicates that an entity wants to participate in tab navigation.
54
///
55
/// Note that you must also add the [`TabGroup`] component to the entity's ancestor in order
56
/// for this component to have any effect.
57
#[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
)]
63
pub struct TabIndex(pub i32);
64
65
/// A component used to mark a tree of entities as containing tabbable elements.
66
#[derive(Debug, Default, Component, Copy, Clone)]
67
#[cfg_attr(
68
feature = "bevy_reflect",
69
derive(Reflect),
70
reflect(Debug, Default, Component, Clone)
71
)]
72
pub struct TabGroup {
73
/// The order of the tab group relative to other tab groups.
74
pub order: i32,
75
76
/// Whether this is a 'modal' group. If true, then tabbing within the group (that is,
77
/// if the current focus entity is a child of this group) will cycle through the children
78
/// of this group. If false, then tabbing within the group will cycle through all non-modal
79
/// tab groups.
80
pub modal: bool,
81
}
82
83
impl TabGroup {
84
/// Create a new tab group with the given order.
85
pub fn new(order: i32) -> Self {
86
Self {
87
order,
88
modal: false,
89
}
90
}
91
92
/// Create a modal tab group.
93
pub fn modal() -> Self {
94
Self {
95
order: 0,
96
modal: true,
97
}
98
}
99
}
100
101
/// A navigation action that users might take to navigate your user interface in a cyclic fashion.
102
///
103
/// These values are consumed by the [`TabNavigation`] system param.
104
#[derive(Clone, Copy)]
105
pub enum NavAction {
106
/// Navigate to the next focusable entity, wrapping around to the beginning if at the end.
107
///
108
/// This is commonly triggered by pressing the Tab key.
109
Next,
110
/// Navigate to the previous focusable entity, wrapping around to the end if at the beginning.
111
///
112
/// This is commonly triggered by pressing Shift+Tab.
113
Previous,
114
/// Navigate to the first focusable entity.
115
///
116
/// This is commonly triggered by pressing Home.
117
First,
118
/// Navigate to the last focusable entity.
119
///
120
/// This is commonly triggered by pressing End.
121
Last,
122
}
123
124
/// An error that can occur during [tab navigation](crate::tab_navigation).
125
#[derive(Debug, Error, PartialEq, Eq, Clone)]
126
pub enum TabNavigationError {
127
/// No tab groups were found.
128
#[error("No tab groups found")]
129
NoTabGroups,
130
/// No focusable entities were found.
131
#[error("No focusable entities found")]
132
NoFocusableEntities,
133
/// Could not navigate to the next focusable entity.
134
///
135
/// This can occur if your tab groups are malformed.
136
#[error("Failed to navigate to next focusable entity")]
137
FailedToNavigateToNextFocusableEntity,
138
/// No tab group for the current focus entity was found.
139
#[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
/// The entity that was previously focused,
142
/// and is missing its tab group.
143
previous_focus: Entity,
144
/// The new entity that will be focused.
145
///
146
/// If you want to recover from this error, set [`InputFocus`] to this entity.
147
new_focus: Entity,
148
},
149
}
150
151
/// An injectable helper object that provides tab navigation functionality.
152
#[doc(hidden)]
153
#[derive(SystemParam)]
154
pub struct TabNavigation<'w, 's> {
155
// Query for tab groups.
156
tabgroup_query: Query<'w, 's, (Entity, &'static TabGroup, &'static Children)>,
157
// Query for tab indices.
158
tabindex_query: Query<
159
'w,
160
's,
161
(Entity, Option<&'static TabIndex>, Option<&'static Children>),
162
Without<TabGroup>,
163
>,
164
// Query for parents.
165
parent_query: Query<'w, 's, &'static ChildOf>,
166
}
167
168
impl TabNavigation<'_, '_> {
169
/// Navigate to the desired focusable entity, relative to the current focused entity.
170
///
171
/// Change the [`NavAction`] to navigate in a different direction.
172
/// Focusable entities are determined by the presence of the [`TabIndex`] component.
173
///
174
/// If there is no currently focused entity, then this function will return either the first
175
/// or last focusable entity, depending on the direction of navigation. For example, if
176
/// `action` is `Next` and no focusable entities are found, then this function will return
177
/// the first focusable entity.
178
pub fn navigate(
179
&self,
180
focus: &InputFocus,
181
action: NavAction,
182
) -> Result<Entity, TabNavigationError> {
183
// If there are no tab groups, then there are no focusable entities.
184
if self.tabgroup_query.is_empty() {
185
return Err(TabNavigationError::NoTabGroups);
186
}
187
188
// Start by identifying which tab group we are in. Mainly what we want to know is if
189
// we're in a modal group.
190
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
/// Initialize focus to a focusable child of a container, either the first or last
205
/// depending on [`NavAction`]. This assumes that the parent entity has a [`TabGroup`]
206
/// component.
207
///
208
/// Focusable entities are determined by the presence of the [`TabIndex`] component.
209
pub fn initialize(
210
&self,
211
parent: Entity,
212
action: NavAction,
213
) -> Result<Entity, TabNavigationError> {
214
// If there are no tab groups, then there are no focusable entities.
215
if self.tabgroup_query.is_empty() {
216
return Err(TabNavigationError::NoTabGroups);
217
}
218
219
// Look for the tab group on the parent entity.
220
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
// List of all focusable entities found.
258
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
// We're in a modal tab group, then gather all tab indices in that group.
264
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
// Otherwise, gather all tab indices in all non-modal tab groups.
272
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
// Stable sort by group order
279
tab_groups.sort_by_key(|(_, tg)| tg.order);
280
281
// Search group descendants
282
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
// Sort by TabGroup and then TabIndex
296
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
/// Gather all focusable entities in tree order.
319
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
// Don't traverse into tab groups, as they are handled separately.
334
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
349
/// Observer which sets focus to the nearest ancestor that has tab index, using bubbling.
350
pub(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 the entity has a TabIndex
357
if focusable.contains(acquire_focus.focused_entity) {
358
// Stop and focus it
359
acquire_focus.propagate(false);
360
// Don't mutate unless we need to, for change detection
361
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
// Stop and clear focus
366
acquire_focus.propagate(false);
367
// Don't mutate unless we need to, for change detection
368
if focus.0.is_some() {
369
focus.clear();
370
}
371
}
372
}
373
374
/// Plugin for navigating between focusable entities using keyboard input.
375
pub struct TabNavigationPlugin;
376
377
impl 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
386
fn 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")]
393
fn 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
// Because `Pointer` is a bubbling event, we don't want to trigger an `AcquireFocus` event
400
// for every ancestor, but only for the original entity. Also, users may want to stop
401
// propagation on the pointer event at some point along the bubbling chain, so we need our
402
// own dedicated event whose propagation we can control.
403
if press.entity == press.original_event_target() {
404
// Clicking hides focus
405
if focus_visible.0 {
406
focus_visible.0 = false;
407
}
408
// Search for a focusable parent entity, defaulting to window if none.
409
if let Ok(window) = windows.single() {
410
commands.trigger(AcquireFocus {
411
focused_entity: press.entity,
412
window,
413
});
414
}
415
}
416
}
417
418
/// Observer function which handles tab navigation.
419
///
420
/// This observer responds to [`KeyCode::Tab`] events and Shift+Tab events,
421
/// cycling through focusable entities in the order determined by their tab index.
422
///
423
/// Any [`TabNavigationError`]s that occur during tab navigation are logged as warnings.
424
pub 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
// Tab navigation.
432
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
// This failure mode is recoverable, but still indicates a problem.
455
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)]
466
mod 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
}
541
542